From f4ed57a9ca615a33bf481f2df8580405ebbf1f4c Mon Sep 17 00:00:00 2001 From: larryro <371767072@qq.com> Date: Tue, 1 Sep 2026 09:40:49 +0800 Subject: [PATCH 1/5] refactor(platform): retire the convex/values validator vocabulary --- .../governance/components/trash-page.tsx | 2 +- .../settings/governance/hooks/queries.ts | 2 +- .../backend/domains/connectors/oauth-apps.ts | 2 +- .../backend/domains/sandbox/sessions.ts | 4 +- .../backend/domains/tasks/comments.ts | 16 +- .../backend/domains/websites/service.ts | 2 +- services/platform/backend/rest/v1-websites.ts | 2 +- .../platform/convex/agents/file_actions.ts | 17 +- services/platform/convex/agents/validators.ts | 177 ---------- services/platform/convex/agents/views.ts | 80 +++++ services/platform/convex/approvals/types.ts | 61 ++++ .../platform/convex/approvals/validators.ts | 76 ----- services/platform/convex/audit_logs/schema.ts | 24 -- services/platform/convex/audit_logs/types.ts | 105 +++++- .../platform/convex/audit_logs/validators.ts | 78 ----- .../convex/automations_builder/run_session.ts | 28 +- .../platform/convex/chat/assistant_tools.ts | 7 +- services/platform/convex/chat/composer.ts | 56 ++-- services/platform/convex/chat/schema.ts | 39 --- .../convex/cloud_import/deployment_config.ts | 2 +- .../platform/convex/cloud_import/providers.ts | 2 +- .../cloud_import/{schema.ts => types.ts} | 6 - services/platform/convex/collab/coalesce.ts | 5 +- .../convex/collab/{schema.ts => types.ts} | 75 ++--- .../connector_catalog.ts | 50 ++- .../{schema.ts => types.ts} | 25 +- .../ingest/build_conversation_metadata.ts | 8 +- .../ingest/build_email_metadata.ts | 8 +- .../platform/convex/conversations/types.ts | 145 +++++++-- .../convex/conversations/validators.ts | 156 --------- services/platform/convex/documents/access.ts | 31 +- .../convex/enterprise_sso/validators.ts | 52 --- ...ft_delete_validators.ts => soft_delete.ts} | 15 +- .../convex/lib/providers/harness_status.ts | 44 ++- .../convex/lib/storage/blob_delete.ts | 4 +- .../platform/convex/lib/storage/blob_ref.ts | 59 ++-- .../platform/convex/lib/type_cast_helpers.ts | 42 --- .../platform/convex/lib/validators/json.ts | 16 - .../convex/node_only/sandbox/session_exec.ts | 2 +- .../platform/convex/notifications/helpers.ts | 8 +- .../notifications/{schema.ts => types.ts} | 15 +- ...essions_schema.ts => session_constants.ts} | 17 - services/platform/convex/sandbox/wire.ts | 308 ------------------ .../convex/sandbox/workspace_access.ts | 19 -- .../platform/convex/skills/file_actions.ts | 2 +- services/platform/convex/skills/validators.ts | 174 ---------- services/platform/convex/skills/views.ts | 76 +++++ services/platform/convex/tasks/access.ts | 6 +- services/platform/convex/tasks/schema.ts | 191 ----------- services/platform/convex/tasks/types.ts | 23 ++ services/platform/convex/websites/types.ts | 64 +++- .../platform/convex/websites/validators.ts | 64 ---- services/platform/lib/harnesses/timeline.ts | 2 +- .../lib/shared/schemas/utils/json-value.ts | 9 - 54 files changed, 678 insertions(+), 1825 deletions(-) delete mode 100644 services/platform/convex/agents/validators.ts create mode 100644 services/platform/convex/agents/views.ts create mode 100644 services/platform/convex/approvals/types.ts delete mode 100644 services/platform/convex/approvals/validators.ts delete mode 100644 services/platform/convex/audit_logs/schema.ts delete mode 100644 services/platform/convex/audit_logs/validators.ts delete mode 100644 services/platform/convex/chat/schema.ts rename services/platform/convex/cloud_import/{schema.ts => types.ts} (59%) rename services/platform/convex/collab/{schema.ts => types.ts} (57%) rename services/platform/convex/connector_credentials/{schema.ts => types.ts} (75%) delete mode 100644 services/platform/convex/conversations/validators.ts delete mode 100644 services/platform/convex/enterprise_sso/validators.ts rename services/platform/convex/governance/{soft_delete_validators.ts => soft_delete.ts} (80%) delete mode 100644 services/platform/convex/lib/type_cast_helpers.ts delete mode 100644 services/platform/convex/lib/validators/json.ts rename services/platform/convex/notifications/{schema.ts => types.ts} (59%) rename services/platform/convex/sandbox/{sessions_schema.ts => session_constants.ts} (85%) delete mode 100644 services/platform/convex/sandbox/wire.ts delete mode 100644 services/platform/convex/skills/validators.ts create mode 100644 services/platform/convex/skills/views.ts delete mode 100644 services/platform/convex/tasks/schema.ts create mode 100644 services/platform/convex/tasks/types.ts delete mode 100644 services/platform/convex/websites/validators.ts delete mode 100644 services/platform/lib/shared/schemas/utils/json-value.ts diff --git a/services/platform/app/features/settings/governance/components/trash-page.tsx b/services/platform/app/features/settings/governance/components/trash-page.tsx index 3142f580db..5980adeace 100644 --- a/services/platform/app/features/settings/governance/components/trash-page.tsx +++ b/services/platform/app/features/settings/governance/components/trash-page.tsx @@ -19,7 +19,7 @@ import { useToast } from '@/app/hooks/use-toast'; import { SOFT_DELETE_RESOURCE_TYPES, type SoftDeleteResourceType, -} from '@/convex/governance/soft_delete_validators'; +} from '@/convex/governance/soft_delete'; import { useT } from '@/lib/i18n/client'; import { mapGovernanceSaveError } from '../governance-save-errors'; diff --git a/services/platform/app/features/settings/governance/hooks/queries.ts b/services/platform/app/features/settings/governance/hooks/queries.ts index 1be1a7e0ef..a3f2de3bb0 100644 --- a/services/platform/app/features/settings/governance/hooks/queries.ts +++ b/services/platform/app/features/settings/governance/hooks/queries.ts @@ -4,7 +4,7 @@ import { useActionQuery } from '@/app/hooks/use-action-query'; import { useBackendQuery } from '@/app/hooks/use-backend-query'; import { useCachedPaginatedQuery } from '@/app/hooks/use-cached-paginated-query'; import type { GOVERNANCE_POLICY_TYPES } from '@/convex/governance/schema'; -import type { SoftDeleteResourceType } from '@/convex/governance/soft_delete_validators'; +import type { SoftDeleteResourceType } from '@/convex/governance/soft_delete'; import { CHAT_MAX_FILE_SIZE, CHAT_UPLOAD_ALLOWED_TYPES, diff --git a/services/platform/backend/domains/connectors/oauth-apps.ts b/services/platform/backend/domains/connectors/oauth-apps.ts index e1e06567c6..f64dba049c 100644 --- a/services/platform/backend/domains/connectors/oauth-apps.ts +++ b/services/platform/backend/domains/connectors/oauth-apps.ts @@ -2,7 +2,7 @@ import type { Sql, TransactionSql } from 'postgres'; import { z } from 'zod'; import { resolveCloudImportOauthApp } from '../../../convex/cloud_import/deployment_config.ts'; -import type { CloudImportProvider } from '../../../convex/cloud_import/schema.ts'; +import type { CloudImportProvider } from '../../../convex/cloud_import/types.ts'; import { maskSecret } from '../../../convex/connector_credentials/masking.ts'; import { resolveOauthAppCredentials } from '../../../convex/http_connectors/deployment_config.ts'; import { diff --git a/services/platform/backend/domains/sandbox/sessions.ts b/services/platform/backend/domains/sandbox/sessions.ts index 76f0968a27..e50dd51e44 100644 --- a/services/platform/backend/domains/sandbox/sessions.ts +++ b/services/platform/backend/domains/sandbox/sessions.ts @@ -7,12 +7,12 @@ import { DEFAULT_SANDBOX_QUOTA, type SessionBudget, } from '../../../convex/sandbox/quota_policy.ts'; -import { sessionIdForWorkflowExecution } from '../../../convex/sandbox/session_naming.ts'; import { SANDBOX_MAX_SESSIONS_PER_OWNER, SANDBOX_SESSION_LIVE_STATUSES, SANDBOX_SESSION_MAX_LIFETIME_MS, -} from '../../../convex/sandbox/sessions_schema.ts'; +} from '../../../convex/sandbox/session_constants.ts'; +import { sessionIdForWorkflowExecution } from '../../../convex/sandbox/session_naming.ts'; import type { SandboxQuotaConfig } from '../../../lib/shared/schemas/governance.ts'; import { toJson } from '../../db/sql.ts'; import { readGovernancePolicyForOrg } from '../../lib/org-config.ts'; diff --git a/services/platform/backend/domains/tasks/comments.ts b/services/platform/backend/domains/tasks/comments.ts index 3a43f07026..40b53883e0 100644 --- a/services/platform/backend/domains/tasks/comments.ts +++ b/services/platform/backend/domains/tasks/comments.ts @@ -1,6 +1,7 @@ import type { Sql, TransactionSql } from 'postgres'; import { TASK_AUDIT_ACTIONS } from '../../../convex/tasks/audit_actions.ts'; +import type { CommentEventComment } from '../../../convex/tasks/types.ts'; import { parseTaskSubjectContract } from '../../../lib/shared/schemas/task_contract.ts'; import { toJson } from '../../db/sql.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; @@ -191,17 +192,16 @@ export async function addTaskComment( metadata: { taskId: args.taskId }, status: 'success', }); + const comment: CommentEventComment = { + body, + projectId: task.projectId, + taskId: args.taskId, + mentions, + }; await emitEvent(tx, { organizationId: auth.organizationId, eventType: 'comment.created', - eventData: { - comment: { - body, - projectId: task.projectId, - taskId: args.taskId, - mentions, - }, - }, + eventData: { comment }, }); await emitHintInTx(tx, { orgId: auth.organizationId, diff --git a/services/platform/backend/domains/websites/service.ts b/services/platform/backend/domains/websites/service.ts index bedc29e150..a22346fcfe 100644 --- a/services/platform/backend/domains/websites/service.ts +++ b/services/platform/backend/domains/websites/service.ts @@ -30,7 +30,7 @@ import { import { isValidScanInterval, SCAN_INTERVAL_VALUES, -} from '../../../convex/websites/validators.ts'; +} from '../../../convex/websites/types.ts'; import { metaDescription, normalizeListedUrl, diff --git a/services/platform/backend/rest/v1-websites.ts b/services/platform/backend/rest/v1-websites.ts index b9c985e607..1b83d64d0d 100644 --- a/services/platform/backend/rest/v1-websites.ts +++ b/services/platform/backend/rest/v1-websites.ts @@ -4,7 +4,7 @@ import type { Sql } from 'postgres'; import { isValidScanInterval, SCAN_INTERVAL_VALUES, -} from '../../convex/websites/validators.ts'; +} from '../../convex/websites/types.ts'; import { createWebsiteRow, deregisterAndDeleteWebsite, diff --git a/services/platform/convex/agents/file_actions.ts b/services/platform/convex/agents/file_actions.ts index 91fe4cda7a..323ddbcab5 100644 --- a/services/platform/convex/agents/file_actions.ts +++ b/services/platform/convex/agents/file_actions.ts @@ -34,7 +34,7 @@ import { type AgentListingView, type AgentSummaryView, type ResolvedAgentView, -} from './validators'; +} from './views'; /** * Agents only know `private | org`, so the member's teams never influence @@ -87,10 +87,11 @@ function assertValidSlug(slug: string): void { } } -/** How a caller is identified to the file layer (see `agentViewerArgs`). */ +/** How a caller is identified to the file layer. */ export interface AgentCallerArgs { orgSlug: string; viewerUserId: string; + /** True when the member may administer the org's shared configuration. */ isOrgAdmin: boolean; } @@ -177,14 +178,24 @@ export async function resolveAgentForCaller( * An omitted optional field means "leave it as it is", so an edit that only * changes the instructions cannot blank the icon or widen a binding list. */ -/** The edit surface (see `agentEditArgs` for the field semantics). */ +/** The edit surface. Everything else in the file round-trips. */ export interface AgentEditInput { displayName: string; description?: string; instructions?: string; + /** + * Absent keeps an existing agent's current visibility and makes a new one + * `private` — an agent starts as its author's own, and sharing it is an + * explicit edit to `org`. + */ visibility?: AgentDefinition['visibility']; icon?: string; labels?: string[]; + /** + * Absent leaves the allowlist as it is; an empty array narrows to nothing; + * `null` REMOVES the narrowing (back to "everything the org offers") — + * without it a widening would be inexpressible, since absent means keep. + */ tools?: string[] | null; skills?: string[] | null; knowledge?: AgentDefinition['knowledge']; diff --git a/services/platform/convex/agents/validators.ts b/services/platform/convex/agents/validators.ts deleted file mode 100644 index 8720bbe248..0000000000 --- a/services/platform/convex/agents/validators.ts +++ /dev/null @@ -1,177 +0,0 @@ -/** - * Wire shapes shared by the `agents` domain's public actions and the - * `'use node'` file actions behind them. - * - * Kept in its own module (no `'use node'`, no filesystem) so both layers - * import the same validators without either pulling the other's runtime in. - * Nothing here describes an execution: an agent crosses the wire as a - * persona — words, bindings, and who may use it — which is all it ever is. - */ - -import { v } from 'convex/values'; - -import type { - AgentKnowledgeScope, - AgentVisibility, -} from '../../lib/shared/schemas/agents'; - -/** - * `private | org` at the wire boundary. The agent schema's - * `AGENT_VISIBILITIES` stays the source of truth for the set; the type - * parameters here fail the build if a literal ever stops belonging to it. - */ -export const agentVisibilityValidator = v.union( - v.literal('private'), - v.literal('org'), -); - -/** Which knowledge an agent's retrieval may read. */ -export const agentKnowledgeScopeValidator = v.union( - v.literal('none'), - v.literal('documents'), - v.literal('web'), - v.literal('all'), -); - -/** The fields every agent view carries. */ -const agentSummaryFields = { - slug: v.string(), - displayName: v.string(), - description: v.optional(v.string()), - visibility: agentVisibilityValidator, - owner: v.optional(v.string()), - icon: v.optional(v.string()), - labels: v.optional(v.array(v.string())), - knowledge: agentKnowledgeScopeValidator, - /** Whether the asking member may change this agent. */ - canEdit: v.boolean(), -}; - -export const agentSummaryValidator = v.object(agentSummaryFields); - -/** - * One agent in full: its authored instructions and its binding lists. An - * ABSENT list means the agent is not narrowed; an empty one means nothing is - * allowed — the distinction survives the wire because both are optional - * arrays, never a defaulted one. - */ -export const agentDocumentValidator = v.object({ - ...agentSummaryFields, - instructions: v.optional(v.string()), - tools: v.optional(v.array(v.string())), - skills: v.optional(v.array(v.string())), - i18n: v.optional( - v.record( - v.string(), - v.object({ - displayName: v.optional(v.string()), - description: v.optional(v.string()), - instructions: v.optional(v.string()), - }), - ), - ), -}); - -/** - * An agent as one turn sees it: localized words plus what it may reach for. - * Deliberately carries no model, no ceiling and no harness — those belong to - * wherever the turn runs. - */ -export const resolvedAgentValidator = v.object({ - slug: v.string(), - displayName: v.string(), - description: v.optional(v.string()), - instructions: v.optional(v.string()), - tools: v.optional(v.array(v.string())), - skills: v.optional(v.array(v.string())), - knowledge: agentKnowledgeScopeValidator, -}); - -/** - * An agent file that failed to load. `path` is relative to the org's config - * tree so an operator can find the file without the server's absolute layout - * being handed to a browser. - */ -export const agentLoadFailureValidator = v.object({ - slug: v.string(), - path: v.string(), - message: v.string(), -}); - -export const agentListingValidator = v.object({ - agents: v.array(agentSummaryValidator), - failures: v.array(agentLoadFailureValidator), -}); - -/** Editable fields of an agent. Everything else in the file round-trips. */ -export const agentEditArgs = { - displayName: v.string(), - description: v.optional(v.string()), - instructions: v.optional(v.string()), - /** - * Absent keeps an existing agent's current visibility and makes a new one - * `private` — an agent starts as its author's own, and sharing it is an - * explicit edit to `org`. - */ - visibility: v.optional(agentVisibilityValidator), - icon: v.optional(v.string()), - labels: v.optional(v.array(v.string())), - /** - * Absent leaves the allowlist as it is; an empty array narrows to nothing; - * `null` REMOVES the narrowing (back to "everything the org offers") — - * without it a widening would be inexpressible, since absent means keep. - */ - tools: v.optional(v.union(v.array(v.string()), v.null())), - skills: v.optional(v.union(v.array(v.string()), v.null())), - knowledge: v.optional(agentKnowledgeScopeValidator), -}; - -/** How the caller is identified to the file layer behind a public action. */ -export const agentViewerArgs = { - viewerUserId: v.string(), - /** True when the member may administer the org's shared configuration. */ - isOrgAdmin: v.boolean(), -}; - -export interface AgentSummaryView { - slug: string; - displayName: string; - description?: string; - visibility: AgentVisibility; - owner?: string; - icon?: string; - labels?: string[]; - knowledge: AgentKnowledgeScope; - canEdit: boolean; -} - -export interface AgentDocumentView extends AgentSummaryView { - instructions?: string; - tools?: string[]; - skills?: string[]; - i18n?: Record< - string, - { displayName?: string; description?: string; instructions?: string } - >; -} - -export interface ResolvedAgentView { - slug: string; - displayName: string; - description?: string; - instructions?: string; - tools?: string[]; - skills?: string[]; - knowledge: AgentKnowledgeScope; -} - -export interface AgentLoadFailureView { - slug: string; - path: string; - message: string; -} - -export interface AgentListingView { - agents: AgentSummaryView[]; - failures: AgentLoadFailureView[]; -} diff --git a/services/platform/convex/agents/views.ts b/services/platform/convex/agents/views.ts new file mode 100644 index 0000000000..3836969cd0 --- /dev/null +++ b/services/platform/convex/agents/views.ts @@ -0,0 +1,80 @@ +/** + * Wire shapes shared by the `agents` domain's routes and the file actions + * behind them. + * + * Kept in its own module (no filesystem) so both layers import the same + * shapes without either pulling the other's runtime in. Nothing here + * describes an execution: an agent crosses the wire as a persona — words, + * bindings, and who may use it — which is all it ever is. + */ + +import type { + AgentKnowledgeScope, + AgentVisibility, +} from '../../lib/shared/schemas/agents'; + +/** The fields every agent view carries. */ +export interface AgentSummaryView { + slug: string; + displayName: string; + description?: string; + /** + * `private | org`. The agent schema's `AGENT_VISIBILITIES` stays the source + * of truth for the set. + */ + visibility: AgentVisibility; + owner?: string; + icon?: string; + labels?: string[]; + /** Which knowledge an agent's retrieval may read. */ + knowledge: AgentKnowledgeScope; + /** Whether the asking member may change this agent. */ + canEdit: boolean; +} + +/** + * One agent in full: its authored instructions and its binding lists. An + * ABSENT list means the agent is not narrowed; an empty one means nothing is + * allowed — the distinction survives the wire because both are optional + * arrays, never a defaulted one. + */ +export interface AgentDocumentView extends AgentSummaryView { + instructions?: string; + tools?: string[]; + skills?: string[]; + i18n?: Record< + string, + { displayName?: string; description?: string; instructions?: string } + >; +} + +/** + * An agent as one turn sees it: localized words plus what it may reach for. + * Deliberately carries no model, no ceiling and no harness — those belong to + * wherever the turn runs. + */ +export interface ResolvedAgentView { + slug: string; + displayName: string; + description?: string; + instructions?: string; + tools?: string[]; + skills?: string[]; + knowledge: AgentKnowledgeScope; +} + +/** + * An agent file that failed to load. `path` is relative to the org's config + * tree so an operator can find the file without the server's absolute layout + * being handed to a browser. + */ +export interface AgentLoadFailureView { + slug: string; + path: string; + message: string; +} + +export interface AgentListingView { + agents: AgentSummaryView[]; + failures: AgentLoadFailureView[]; +} diff --git a/services/platform/convex/approvals/types.ts b/services/platform/convex/approvals/types.ts new file mode 100644 index 0000000000..8062e059ae --- /dev/null +++ b/services/platform/convex/approvals/types.ts @@ -0,0 +1,61 @@ +/** + * Approval-operation vocabulary. Zod schemas for client-side validation live + * in lib/shared/schemas/approvals.ts. + */ + +export type ApprovalStatus = 'pending' | 'executing' | 'completed' | 'rejected'; + +export type ApprovalPriority = 'low' | 'medium' | 'high' | 'urgent'; + +export type ApprovalResourceType = + | 'conversations' + | 'connector_operation' + | 'workflow_creation' + | 'workflow_run' + | 'workflow_update' + | 'human_input_request' + | 'document_write' + | 'knowledge_write' + | 'location_request' + | 'mcp_tool_call' + // GDPR Art 17 erasure request awaiting dual-admin approval. Used when + // `dsar_governance.requireDualApproval` is enabled at the org level. + | 'erasure' + // Task-ops review gate: agent work parked at in_review awaiting a human + // approve / request-changes decision. resourceId = String(taskId). + | 'task_review' + // Controlled-record review gate: a document record submitted for review + // (documents/records.ts). resourceId = String(documentId); respondable + // ONLY via respondToDocumentRecordReview — updateApprovalStatus refuses. + | 'document_record_review' + // External-agent (Claude Code) plan proposal awaiting the user's + // approve-and-execute in chat (plan/act workflow). + | 'external_agent_plan' + // External-agent browser handoff: the agent parked its turn to let a human + // drive the live browser (CAPTCHA/login/2FA); returning control resumes it. + | 'external_agent_human_control' + // Operator-input marker: a run parked to ask the operator a question it + // answers on the task timeline (comment loop), not via an approval card. + // Lights the "Needs your input" run indicator; keyed to the execution so it + // clears when a newer run supersedes it. resourceId = String(taskId). + | 'operator_input'; + +export interface ApprovalItem { + _id: string; + _creationTime: number; + organizationId: string; + wfExecutionId?: string; + stepSlug?: string; + status: ApprovalStatus; + approvedBy?: string; + reviewedAt?: number; + resourceType: ApprovalResourceType; + resourceId: string; + priority: ApprovalPriority; + dueDate?: number; + executedAt?: number; + executionError?: string; + metadata?: Record; + threadId?: string; + messageId?: string; +} diff --git a/services/platform/convex/approvals/validators.ts b/services/platform/convex/approvals/validators.ts deleted file mode 100644 index 6a0e7b77b7..0000000000 --- a/services/platform/convex/approvals/validators.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Convex validators for approval operations - * - * Uses native Convex v.* validators to avoid pulling zod into the query bundle. - * Zod schemas for client-side validation live in lib/shared/schemas/approvals.ts. - */ - -import { v } from 'convex/values'; - -export const approvalStatusValidator = v.union( - v.literal('pending'), - v.literal('executing'), - v.literal('completed'), - v.literal('rejected'), -); - -export const approvalPriorityValidator = v.union( - v.literal('low'), - v.literal('medium'), - v.literal('high'), - v.literal('urgent'), -); - -export const approvalResourceTypeValidator = v.union( - v.literal('conversations'), - v.literal('connector_operation'), - v.literal('workflow_creation'), - v.literal('workflow_run'), - v.literal('workflow_update'), - v.literal('human_input_request'), - v.literal('document_write'), - v.literal('knowledge_write'), - v.literal('location_request'), - v.literal('mcp_tool_call'), - // GDPR Art 17 erasure request awaiting dual-admin approval. Used when - // `dsar_governance.requireDualApproval` is enabled at the org level. - v.literal('erasure'), - // Task-ops review gate: agent work parked at in_review awaiting a human - // approve / request-changes decision. resourceId = String(taskId). - v.literal('task_review'), - // Controlled-record review gate: a document record submitted for review - // (documents/records.ts). resourceId = String(documentId); respondable - // ONLY via respondToDocumentRecordReview — updateApprovalStatus refuses. - v.literal('document_record_review'), - // External-agent (Claude Code) plan proposal awaiting the user's - // approve-and-execute in chat (plan/act workflow). - v.literal('external_agent_plan'), - // External-agent browser handoff: the agent parked its turn to let a human - // drive the live browser (CAPTCHA/login/2FA); returning control resumes it. - v.literal('external_agent_human_control'), - // Operator-input marker: a run parked to ask the operator a question it - // answers on the task timeline (comment loop), not via an approval card. - // Lights the "Needs your input" run indicator; keyed to the execution so it - // clears when a newer run supersedes it. resourceId = String(taskId). - v.literal('operator_input'), -); - -export const approvalItemValidator = v.object({ - _id: v.string(), - _creationTime: v.number(), - organizationId: v.string(), - wfExecutionId: v.optional(v.string()), - stepSlug: v.optional(v.string()), - status: approvalStatusValidator, - approvedBy: v.optional(v.string()), - reviewedAt: v.optional(v.number()), - resourceType: approvalResourceTypeValidator, - resourceId: v.string(), - priority: approvalPriorityValidator, - dueDate: v.optional(v.number()), - executedAt: v.optional(v.number()), - executionError: v.optional(v.string()), - metadata: v.optional(v.any()), - threadId: v.optional(v.string()), - messageId: v.optional(v.string()), -}); diff --git a/services/platform/convex/audit_logs/schema.ts b/services/platform/convex/audit_logs/schema.ts deleted file mode 100644 index f88e1e74a2..0000000000 --- a/services/platform/convex/audit_logs/schema.ts +++ /dev/null @@ -1,24 +0,0 @@ -export const AUDIT_LOG_ACTOR_TYPES = [ - 'user', - 'system', - 'api', - 'workflow', -] as const; -export const AUDIT_LOG_CATEGORIES = [ - 'auth', - 'member', - 'data', - 'connector', - // Legacy spelling of `connector` from before the integration→connector - // rename (#2876). 0.4 deploys never write it; accepted so pre-rename LOCAL - // dev rows keep validating (audit rows are immutable history — a hash - // chain — so they are read as-is rather than rewritten). - 'integration', - 'workflow', - 'security', - 'admin', - 'ai', - 'skill', - 'agent', -] as const; -export const AUDIT_LOG_STATUSES = ['success', 'failure', 'denied'] as const; diff --git a/services/platform/convex/audit_logs/types.ts b/services/platform/convex/audit_logs/types.ts index a45d9a1ac5..1acb01da75 100644 --- a/services/platform/convex/audit_logs/types.ts +++ b/services/platform/convex/audit_logs/types.ts @@ -1,18 +1,93 @@ -import type { Infer } from 'convex/values'; - -import type { - auditLogActorTypeValidator, - auditLogCategoryValidator, - auditLogStatusValidator, - auditLogItemValidator, - auditLogFilterValidator, -} from './validators'; - -export type AuditLogActorType = Infer; -export type AuditLogCategory = Infer; -export type AuditLogStatus = Infer; -export type AuditLogItem = Infer; -export type AuditLogFilter = Infer; +import type { SoftDeleteStatus } from '../governance/soft_delete'; + +export const AUDIT_LOG_ACTOR_TYPES = [ + 'user', + 'system', + 'api', + 'workflow', +] as const; +export const AUDIT_LOG_CATEGORIES = [ + 'auth', + 'member', + 'data', + 'connector', + // Legacy spelling of `connector` from before the integration→connector + // rename (#2876). 0.4 deploys never write it; accepted so pre-rename LOCAL + // dev rows keep validating (audit rows are immutable history — a hash + // chain — so they are read as-is rather than rewritten). + 'integration', + 'workflow', + 'security', + 'admin', + 'ai', + 'skill', + 'agent', +] as const; +export const AUDIT_LOG_STATUSES = ['success', 'failure', 'denied'] as const; + +export type AuditLogActorType = (typeof AUDIT_LOG_ACTOR_TYPES)[number]; +export type AuditLogCategory = (typeof AUDIT_LOG_CATEGORIES)[number]; +export type AuditLogStatus = (typeof AUDIT_LOG_STATUSES)[number]; + +/** One audit-log row as the read surfaces return it. */ +export interface AuditLogItem { + _id: string; + _creationTime: number; + organizationId: string; + + actorId: string; + actorEmail?: string; + actorRole?: string; + actorType: AuditLogActorType; + + action: string; + category: AuditLogCategory; + + resourceType: string; + resourceId?: string; + resourceName?: string; + + previousState?: Record; + newState?: Record; + changedFields?: string[]; + + sessionId?: string; + ipAddress?: string; + userAgent?: string; + requestId?: string; + + timestamp: number; + status: AuditLogStatus; + errorMessage?: string; + metadata?: Record; + + integrityHash?: string; + previousHash?: string; + chainSuccessor?: string; + piiScrubbed?: boolean; + piiScrubbedAt?: number; + + actorEmailHash?: string; + actorIpHash?: string; + + // Patched onto the row by retention soft-delete (`markRowExpiredGeneric`). + // Excluded from the integrity hash via `EXCLUDED_FIELDS` in + // `audit_hash.ts`; declared here so read projections don't reject + // soft-deleted rows. + lifecycleStatus?: SoftDeleteStatus; + statusChangedAt?: number; +} + +export interface AuditLogFilter { + category?: AuditLogCategory; + actorId?: string; + resourceType?: string; + resourceId?: string; + status?: AuditLogStatus; + startDate?: number; + endDate?: number; + search?: string; +} export interface CreateAuditLogArgs { organizationId: string; diff --git a/services/platform/convex/audit_logs/validators.ts b/services/platform/convex/audit_logs/validators.ts deleted file mode 100644 index 9734582cba..0000000000 --- a/services/platform/convex/audit_logs/validators.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { v } from 'convex/values'; - -import { lifecycleStatusValidator } from '../governance/soft_delete_validators'; -import { jsonRecordValidator } from '../lib/validators/json'; -import { - AUDIT_LOG_ACTOR_TYPES, - AUDIT_LOG_CATEGORIES, - AUDIT_LOG_STATUSES, -} from './schema'; - -export const auditLogActorTypeValidator = v.union( - ...AUDIT_LOG_ACTOR_TYPES.map((t) => v.literal(t)), -); -export const auditLogCategoryValidator = v.union( - ...AUDIT_LOG_CATEGORIES.map((c) => v.literal(c)), -); -export const auditLogStatusValidator = v.union( - ...AUDIT_LOG_STATUSES.map((s) => v.literal(s)), -); - -export const auditLogItemValidator = v.object({ - _id: v.id('auditLogs'), - _creationTime: v.number(), - organizationId: v.string(), - - actorId: v.string(), - actorEmail: v.optional(v.string()), - actorRole: v.optional(v.string()), - actorType: auditLogActorTypeValidator, - - action: v.string(), - category: auditLogCategoryValidator, - - resourceType: v.string(), - resourceId: v.optional(v.string()), - resourceName: v.optional(v.string()), - - previousState: v.optional(jsonRecordValidator), - newState: v.optional(jsonRecordValidator), - changedFields: v.optional(v.array(v.string())), - - sessionId: v.optional(v.string()), - ipAddress: v.optional(v.string()), - userAgent: v.optional(v.string()), - requestId: v.optional(v.string()), - - timestamp: v.number(), - status: auditLogStatusValidator, - errorMessage: v.optional(v.string()), - metadata: v.optional(jsonRecordValidator), - - integrityHash: v.optional(v.string()), - previousHash: v.optional(v.string()), - chainSuccessor: v.optional(v.id('auditLogs')), - piiScrubbed: v.optional(v.boolean()), - piiScrubbedAt: v.optional(v.number()), - - actorEmailHash: v.optional(v.string()), - actorIpHash: v.optional(v.string()), - - // Patched onto the row by retention soft-delete (`markRowExpiredGeneric`). - // Excluded from the integrity hash via `EXCLUDED_FIELDS` in - // `audit_hash.ts`; declared here so query-return validators don't reject - // soft-deleted rows. - lifecycleStatus: v.optional(lifecycleStatusValidator), - statusChangedAt: v.optional(v.number()), -}); - -export const auditLogFilterValidator = v.object({ - category: v.optional(auditLogCategoryValidator), - actorId: v.optional(v.string()), - resourceType: v.optional(v.string()), - resourceId: v.optional(v.string()), - status: v.optional(auditLogStatusValidator), - startDate: v.optional(v.number()), - endDate: v.optional(v.number()), - search: v.optional(v.string()), -}); diff --git a/services/platform/convex/automations_builder/run_session.ts b/services/platform/convex/automations_builder/run_session.ts index 88c69506d5..fcd0a70674 100644 --- a/services/platform/convex/automations_builder/run_session.ts +++ b/services/platform/convex/automations_builder/run_session.ts @@ -1,7 +1,7 @@ 'use node'; /** - * The Convex host for an automation builder session. + * The server host for an automation builder session. * * Everything that decides how a session behaves lives in * `lib/automations_builder/` and is pure. This module is the wiring: it @@ -21,8 +21,6 @@ * system. Live runs belong to deployment, behind the deploy gate. */ -import { v } from 'convex/values'; - import { runBuilderSession } from '../../lib/automations_builder/session'; import { installConnectorCatalog } from '../../lib/connectors/dispatcher'; import { registerConnector } from '../../lib/connectors/registry'; @@ -120,27 +118,3 @@ export async function runSessionWithStore( ); return outcome; } - -/** The session outcome on the wire — shared by every host that returns one - * (the internal action below, and the client-facing surface in `actions.ts`). */ -export const builderSessionOutcomeValidator = v.object({ - status: v.union( - v.literal('succeeded'), - v.literal('gave-up'), - v.literal('cancelled'), - ), - reason: v.optional(v.string()), - saved: v.optional(v.object({ name: v.string(), version: v.number() })), - turns: v.number(), - restarts: v.number(), - usage: v.object({ prompt: v.number(), completion: v.number() }), - steps: v.array( - v.object({ - turn: v.number(), - kind: v.string(), - method: v.optional(v.string()), - note: v.optional(v.string()), - progress: v.optional(v.boolean()), - }), - ), -}); diff --git a/services/platform/convex/chat/assistant_tools.ts b/services/platform/convex/chat/assistant_tools.ts index ae20de764e..a08219b967 100644 --- a/services/platform/convex/chat/assistant_tools.ts +++ b/services/platform/convex/chat/assistant_tools.ts @@ -58,7 +58,6 @@ import { SafeFetchError, isPrivateIp, safeFetch } from '../lib/http/safe_fetch'; import type { AgentReadSubject } from '../lib/rls/helpers/agent_read_access'; import type { Doc } from '../lib/rows'; import { detectListingIntent } from '../lib/search'; -import { toId } from '../lib/type_cast_helpers'; import { sanitizeUntrustedField, wrapUntrusted, @@ -1314,7 +1313,7 @@ export function createChatToolExecutor( excludeArchived: true, ...(status !== undefined ? { status } : {}), ...(call.projectId !== undefined - ? { projectId: toId<'projects'>(call.projectId) } + ? { projectId: call.projectId } : {}), paginationOpts: { numItems: limit, cursor }, }, @@ -1754,7 +1753,7 @@ export function createChatToolExecutor( try { scoped = await ctx.runQuery( internal.tasks.internal_queries.getTaskByIdInternal, - { taskId: toId<'tasks'>(taskId), organizationId: who.organizationId }, + { taskId: taskId, organizationId: who.organizationId }, ); } catch { await recordDispatch('rag_fetch', missing.status, missing.message); @@ -1847,7 +1846,7 @@ export function createChatToolExecutor( { organizationId: who.organizationId, projectIds: [...access.projectIds], - projectId: toId<'projects'>(projectId), + projectId: projectId, term: '', // An explicit listing, so the page size is honoured (the old // fallback pinned its own cap and `truncated` could never be diff --git a/services/platform/convex/chat/composer.ts b/services/platform/convex/chat/composer.ts index bd79c48d2d..cb2302789f 100644 --- a/services/platform/convex/chat/composer.ts +++ b/services/platform/convex/chat/composer.ts @@ -20,58 +20,46 @@ * custom connectors is filesystem work. */ -import { v, type Infer } from 'convex/values'; - import { walkChatCatalog } from '../lib/providers/chat_catalog'; + /** The forced-execution constraints a subscription credential carries. */ -const executionConstraintsValidator = v.object({ - execution: v.literal('sandbox'), - harness: v.string(), -}); +interface ExecutionConstraints { + execution: 'sandbox'; + harness: string; +} /** * The credential facts execution resolution reads, mirroring * {@link CredentialAuth}: the plain methods carry only their name; the * subscription methods carry the harness they are bound to. */ -const credentialAuthValidator = v.union( - v.object({ authMethod: v.literal('api-key') }), - v.object({ authMethod: v.literal('env') }), - v.object({ - authMethod: v.literal('subscription-key'), - constraints: executionConstraintsValidator, - }), - v.object({ - authMethod: v.literal('subscription-broker'), - constraints: executionConstraintsValidator, - }), -); +type ComposerCredentialAuth = + | { authMethod: 'api-key' } + | { authMethod: 'env' } + | { authMethod: 'subscription-key'; constraints: ExecutionConstraints } + | { authMethod: 'subscription-broker'; constraints: ExecutionConstraints }; -const composerModelOptionValidator = v.object({ - id: v.string(), - label: v.string(), - providerSlug: v.string(), +interface ComposerModelOption { + id: string; + label: string; + providerSlug: string; /** The provider's human name (`displayName` in its yml) — pickers show it * next to each model so two providers serving the same id are tellable * apart. */ - providerLabel: v.string(), - credential: credentialAuthValidator, + providerLabel: string; + credential: ComposerCredentialAuth; /** Present when the model's reasoning depth is controllable — the effort * picker renders only for these. `toolsRequireOff` marks a model whose * endpoint refuses tools+effort together: the picker offers no levels and * says why (the resolver sends the catalog's off value regardless). */ - reasoning: v.optional( - v.object({ - knob: v.union(v.literal('effort'), v.literal('budget-tokens')), - toolsRequireOff: v.optional(v.boolean()), - }), - ), + reasoning?: { + knob: 'effort' | 'budget-tokens'; + toolsRequireOff?: boolean; + }; /** The model can see images (catalog `vision` tag) — the composer warns * when attachments are staged for a model without it. */ - vision: v.optional(v.boolean()), -}); - -type ComposerModelOption = Infer; + vision?: boolean; +} /** * The per-hit projection behind the model picker — pure, so the 0.5 backend * runs it over its own catalog walk. Keyed by (provider, id), first-wins per diff --git a/services/platform/convex/chat/schema.ts b/services/platform/convex/chat/schema.ts deleted file mode 100644 index 63fca835d5..0000000000 --- a/services/platform/convex/chat/schema.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { v } from 'convex/values'; - -/** - * Chat storage — threads, their messages, and the live generation state. - * - * The split is deliberate. `threads` is small, frequently listed, and rarely - * written; `messages` is append-heavy and read as a whole conversation; - * `generations` is the only hot-written row during a turn. Keeping the - * in-flight state out of the thread row means a streaming turn does not - * rewrite a row that every thread list reads. - * - * What is NOT here is as deliberate as what is. There are no routing columns - * (no route reason, no tier): a chat send may say Auto, but that resolves to - * a concrete model BEFORE the turn binds, and only the resolved model is - * recorded (`messages.model`) — the pick's why goes to the server log, not a - * column. No personalization blob and no auto-injected memory or retrieval - * context, because everything the model sees is assembled from the message - * history and the tools it calls; and no per-agent timeout, because - * execution ceilings are physics the host enforces, not policy stored per - * conversation. - */ - -/** Where a thread came from. `sandbox` threads run their turns inside a - * harness session; `direct` threads call the model API. */ -export const chatKindValidator = v.union( - v.literal('direct'), - v.literal('sandbox'), -); - -/** The user-facing reasoning-effort scale — the five steps of - * `lib/chat/effort.ts`, spelled as literals for every arg and column that - * carries a pick. */ -export const reasoningEffortValidator = v.union( - v.literal('low'), - v.literal('medium'), - v.literal('high'), - v.literal('extra'), - v.literal('max'), -); diff --git a/services/platform/convex/cloud_import/deployment_config.ts b/services/platform/convex/cloud_import/deployment_config.ts index 3359c9d411..fa0ea4c7f2 100644 --- a/services/platform/convex/cloud_import/deployment_config.ts +++ b/services/platform/convex/cloud_import/deployment_config.ts @@ -11,7 +11,7 @@ * single-tenant registrations created after 2018-10-15. */ -import type { CloudImportProvider } from './schema'; +import type { CloudImportProvider } from './types'; export const CLOUD_IMPORT_OAUTH_CALLBACK_PATH = '/api/cloud-import/oauth2/callback'; diff --git a/services/platform/convex/cloud_import/providers.ts b/services/platform/convex/cloud_import/providers.ts index 5ea279b99d..b87e391413 100644 --- a/services/platform/convex/cloud_import/providers.ts +++ b/services/platform/convex/cloud_import/providers.ts @@ -8,7 +8,7 @@ * for single-tenant app registrations). */ -import type { CloudImportProvider } from './schema'; +import type { CloudImportProvider } from './types'; export interface CloudImportProviderEndpoints { readonly displayName: string; diff --git a/services/platform/convex/cloud_import/schema.ts b/services/platform/convex/cloud_import/types.ts similarity index 59% rename from services/platform/convex/cloud_import/schema.ts rename to services/platform/convex/cloud_import/types.ts index 6716a7e01f..459eb36f3d 100644 --- a/services/platform/convex/cloud_import/schema.ts +++ b/services/platform/convex/cloud_import/types.ts @@ -1,11 +1,5 @@ -import { v } from 'convex/values'; /** * Cloud providers a member may authorize for Knowledge import/sync. * Distinct from org connectors (shared credentials) and from login identity. */ -export const cloudImportProviderValidator = v.union( - v.literal('onedrive'), - v.literal('google-drive'), -); - export type CloudImportProvider = 'onedrive' | 'google-drive'; diff --git a/services/platform/convex/collab/coalesce.ts b/services/platform/convex/collab/coalesce.ts index 05dd88d4f3..1ace9ae66e 100644 --- a/services/platform/convex/collab/coalesce.ts +++ b/services/platform/convex/collab/coalesce.ts @@ -23,15 +23,12 @@ * of their history, so the next event starts a fresh row. */ -import type { Infer } from 'convex/values'; - import { isActionableNotificationType } from '../../lib/shared/attention'; import type { MutationCtx } from '../lib/ctx'; import { internal } from '../lib/handler_names'; import type { Doc, Id } from '../lib/rows'; -import type { notificationTypeValidator } from './schema'; +import type { NotificationType } from './types'; -type NotificationType = Infer; type ResourceType = Doc<'userNotifications'>['resourceType']; /** diff --git a/services/platform/convex/collab/schema.ts b/services/platform/convex/collab/types.ts similarity index 57% rename from services/platform/convex/collab/schema.ts rename to services/platform/convex/collab/types.ts index db32453d49..2786659818 100644 --- a/services/platform/convex/collab/schema.ts +++ b/services/platform/convex/collab/types.ts @@ -1,78 +1,69 @@ -import { v } from 'convex/values'; /** - * Collaboration tables: per-user content notifications, task subscriptions, and - * per-user notification preferences. + * Collaboration vocabulary: per-user content notifications, task + * subscriptions, and per-user notification preferences. * * Content notifications use a PER-USER row (one row per recipient) rather than * the org-wide `notifications` table's `readBy[]` array, which does not scale to * large orgs. The org `notifications` table stays for system/security alerts. */ -export const notificationTypeValidator = v.union( - v.literal('task_assigned'), +export type NotificationType = + | 'task_assigned' // The assignee was removed and nobody replaced them (or someone else did): // the person who was carrying it is told they no longer are. Bell only — // losing work is not an inbox action. - v.literal('task_unassigned'), - v.literal('task_status_changed'), - v.literal('task_commented'), - v.literal('mention'), + | 'task_unassigned' + | 'task_status_changed' + | 'task_commented' + | 'mention' // Start date reached / due soon / overdue. Its own type (not // `task_status_changed`) so muting board churn can't mute a deadline, and so // it can email the person carrying the work. - v.literal('task_deadline'), - // --- Task-ops automation types. Schema ships one release ahead of the - // emitters (closed-union deploy-order constraint). --- + | 'task_deadline' + // --- Task-ops automation types. --- // Work awaits human review (the in_review gate — agent OR human // submission). Actionable. - v.literal('task_review_requested'), + | 'task_review_requested' // A review the user was watching was approved / sent back. - v.literal('task_review_resolved'), + | 'task_review_resolved' // The user was designated a task's reviewer while the work is still in // flight — a heads-up, so NOT actionable (bell only, no email). The // actionable request follows when the task reaches in_review. - v.literal('task_reviewer_assigned'), + | 'task_reviewer_assigned' // A controlled document was submitted to the user for review // (documents/records.ts). Actionable — the named reviewer must know. - v.literal('document_review_requested'), + | 'document_review_requested' // The user's controlled-document submission was approved / sent back. - v.literal('document_review_resolved'), + | 'document_review_resolved' // An agent needs a human: an automation turn parked on an `ask_human` // question (`collab/notify_agent_asks.ts`), or a root escalation / circuit // breaker. Actionable. - v.literal('agent_escalation'), + | 'agent_escalation' // A task-ops pack workflow execution failed (admins). - v.literal('automation_failed'), + | 'automation_failed' // Agent budget warn/pause threshold crossed (admins). - v.literal('budget_alert'), + | 'budget_alert' // An external agent runtime went offline (admins). - v.literal('runtime_offline'), + | 'runtime_offline' // RETIRED — no emitter writes this type anymore (the digest automation was - // removed) and migration 0.2.90/08 deletes the stored rows. The literal - // stays one release because the closed union validates EXISTING rows at - // schema push time (same deploy-order constraint as adding a type, in - // reverse); drop it in the next release. - v.literal('workforce_digest'), + // removed). The literal stays so stored rows keep typing; drop it once the + // stored rows are gone. + | 'workforce_digest' // Inbound customer message in Conversations (automation-driven). - v.literal('conversation_message'), + | 'conversation_message' // A conversation was assigned to a member by an admin (targeted; the new // assignee is notified, mirroring task_assigned). Actionable. - v.literal('conversation_assigned'), -); + | 'conversation_assigned'; -export const notificationActorTypeValidator = v.union( - v.literal('user'), - v.literal('agent'), - v.literal('system'), -); -export const subscriptionReasonValidator = v.union( - v.literal('creator'), - v.literal('assignee'), - v.literal('commenter'), - v.literal('mention'), +export type NotificationActorType = 'user' | 'agent' | 'system'; + +export type SubscriptionReason = + | 'creator' + | 'assignee' + | 'commenter' + | 'mention' // The designated reviewer follows the task from designation onward: they own // the gate, so they need its progress (comments, status, outcome) — not just // the moment the request lands. - v.literal('reviewer'), - v.literal('manual'), -); + | 'reviewer' + | 'manual'; diff --git a/services/platform/convex/connector_credentials/connector_catalog.ts b/services/platform/convex/connector_credentials/connector_catalog.ts index 6bbb737a1f..91ebd48de6 100644 --- a/services/platform/convex/connector_credentials/connector_catalog.ts +++ b/services/platform/convex/connector_credentials/connector_catalog.ts @@ -19,8 +19,6 @@ import { existsSync, readFileSync } from 'node:fs'; import path from 'node:path'; -import { type Infer, v } from 'convex/values'; - import { connectorBearerScheme, findConnector, @@ -28,20 +26,20 @@ import { resolveConnectorsDir, type LoadConnectorCatalogOptions, } from '../../lib/connectors/catalog'; -import { connectorAuthMethodValidator } from './schema'; +import type { ConnectorAuthMethod } from './types'; export { connectorBearerScheme, findConnector, loadConnectorDefinitions }; export type { LoadConnectorCatalogOptions }; /** One shipped connector as the settings catalog lists it. Mirrors * `ConnectorSummary` in the app's `connectors/hooks/backend.ts`. */ -const connectorSummaryValidator = v.object({ - slug: v.string(), - displayName: v.string(), - description: v.string(), - tags: v.array(v.string()), - endpointMode: v.union(v.literal('fixed'), v.literal('per-credential')), - authMethods: v.array(connectorAuthMethodValidator), +interface ConnectorSummary { + slug: string; + displayName: string; + description: string; + tags: string[]; + endpointMode: 'fixed' | 'per-credential'; + authMethods: ConnectorAuthMethod[]; /** * The connector's non-secret per-credential settings, as declared. The create * form has to RENDER these: `createCredential` validates the submitted config @@ -49,26 +47,18 @@ const connectorSummaryValidator = v.object({ * collect them cannot author a credential for any connector declaring one. * Not secret — labels, types and defaults from the shipped connector. */ - configFields: v.array( - v.object({ - key: v.string(), - label: v.string(), - type: v.union( - v.literal('string'), - v.literal('number'), - v.literal('boolean'), - ), - description: v.optional(v.string()), - required: v.boolean(), - enum: v.optional(v.array(v.string())), - default: v.optional(v.union(v.string(), v.number(), v.boolean())), - }), - ), - actionCount: v.number(), - iconUrl: v.optional(v.string()), -}); - -type ConnectorSummary = Infer; + configFields: Array<{ + key: string; + label: string; + type: 'string' | 'number' | 'boolean'; + description?: string; + required: boolean; + enum?: string[]; + default?: string | number | boolean; + }>; + actionCount: number; + iconUrl?: string; +} /** * The connector's shipped `icon.svg` as an inline data URL, or `undefined` diff --git a/services/platform/convex/connector_credentials/schema.ts b/services/platform/convex/connector_credentials/types.ts similarity index 75% rename from services/platform/convex/connector_credentials/schema.ts rename to services/platform/convex/connector_credentials/types.ts index 8243c6ed07..77c2af1b9e 100644 --- a/services/platform/convex/connector_credentials/schema.ts +++ b/services/platform/convex/connector_credentials/types.ts @@ -1,5 +1,3 @@ -import { v } from 'convex/values'; - /** * Connector credentials — org-owned, MULTIPLE per connector. A row pairs one * shipped connector (`configs/platform/system/connectors//connector.yml`) @@ -17,7 +15,7 @@ import { v } from 'convex/values'; * - `oauth2` — an authorization-code grant: access token, optional refresh * token, expiry, and the granted scopes. * - * Secret material NEVER leaves `'use node'` code: queries return metadata plus + * Secret material NEVER leaves server-side code: queries return metadata plus * the write-time `maskedPreview`; plaintext is reachable only through * `resolve_credential.ts`. Everything secret lives inside the single * `encryptedData` envelope (AES-256-GCM via `lib/secret_box.ts`) rather than @@ -29,20 +27,7 @@ import { v } from 'convex/values'; * a workflow node or chat invocation names one via `credential`, and omitting * it selects the org default for that connector. */ -export const connectorAuthMethodValidator = v.union( - v.literal('api-key'), - v.literal('bearer'), - v.literal('basic'), - v.literal('oauth2'), -); - -/** `lib/secret_box.ts` `EncryptedSecret`, as a Convex validator. */ -export const encryptedSecretValidator = v.object({ - ciphertext: v.string(), - nonce: v.string(), - authTag: v.string(), - keyFingerprint: v.string(), -}); +export type ConnectorAuthMethod = 'api-key' | 'bearer' | 'basic' | 'oauth2'; /** * `disabled` is an operator decision; `needs-reauth` is the system's — an @@ -50,8 +35,4 @@ export const encryptedSecretValidator = v.object({ * one is fixed by re-running the consent flow, and the settings UI must say * which is which instead of showing one ambiguous "broken" state. */ -export const connectorCredentialStatusValidator = v.union( - v.literal('active'), - v.literal('disabled'), - v.literal('needs-reauth'), -); +export type ConnectorCredentialStatus = 'active' | 'disabled' | 'needs-reauth'; diff --git a/services/platform/convex/conversations/ingest/build_conversation_metadata.ts b/services/platform/convex/conversations/ingest/build_conversation_metadata.ts index eba56c9c0c..885c671fc5 100644 --- a/services/platform/convex/conversations/ingest/build_conversation_metadata.ts +++ b/services/platform/convex/conversations/ingest/build_conversation_metadata.ts @@ -1,5 +1,3 @@ -import { toConvexJsonRecord } from '../../lib/type_cast_helpers'; -import type { ConvexJsonRecord } from '../../lib/validators/json'; import { attachmentsForMetadata } from './attachments_for_metadata'; import type { EmailType } from './types'; @@ -9,8 +7,8 @@ import type { EmailType } from './types'; export function buildConversationMetadata( email: EmailType, additionalMetadata?: Record, -): ConvexJsonRecord { - return toConvexJsonRecord({ +): Record { + return { from: email.from, to: email.to, cc: email.cc, @@ -23,5 +21,5 @@ export function buildConversationMetadata( flags: email.flags, attachments: attachmentsForMetadata(email.attachments), ...additionalMetadata, - }); + }; } diff --git a/services/platform/convex/conversations/ingest/build_email_metadata.ts b/services/platform/convex/conversations/ingest/build_email_metadata.ts index fbede60127..df872fed0b 100644 --- a/services/platform/convex/conversations/ingest/build_email_metadata.ts +++ b/services/platform/convex/conversations/ingest/build_email_metadata.ts @@ -1,5 +1,3 @@ -import { toConvexJsonRecord } from '../../lib/type_cast_helpers'; -import type { ConvexJsonRecord } from '../../lib/validators/json'; import { attachmentsForMetadata } from './attachments_for_metadata'; import { NO_SUBJECT } from './constants'; import type { EmailType } from './types'; @@ -8,8 +6,8 @@ import type { EmailType } from './types'; * Build rich metadata object for email message * Preserves both text and HTML content separately in metadata */ -export function buildEmailMetadata(email: EmailType): ConvexJsonRecord { - return toConvexJsonRecord({ +export function buildEmailMetadata(email: EmailType): Record { + return { from: email.from, to: email.to, cc: email.cc, @@ -24,5 +22,5 @@ export function buildEmailMetadata(email: EmailType): ConvexJsonRecord { flags: email.flags, attachments: attachmentsForMetadata(email.attachments), subject: email.subject || NO_SUBJECT, - }); + }; } diff --git a/services/platform/convex/conversations/types.ts b/services/platform/convex/conversations/types.ts index 07221575de..a5dbf67696 100644 --- a/services/platform/convex/conversations/types.ts +++ b/services/platform/convex/conversations/types.ts @@ -1,42 +1,125 @@ /** - * Type definitions for conversation model + * Type definitions for conversation operations. Zod schemas for client-side + * validation live in lib/shared/schemas/conversations.ts. */ -import type { Infer } from 'convex/values'; - +import type { ApprovalItem } from '../approvals/types'; import type { Id } from '../lib/rows'; -import type { - bulkOperationResultValidator, - contactInfoValidator, - conversationItemValidator, - conversationListResponseValidator, - conversationPriorityValidator, - conversationStatusValidator, - conversationWithMessagesValidator, - messageStatusValidator, - messageValidator, -} from './validators'; -// ============================================================================= -// INFERRED TYPES (from validators) -// ============================================================================= +export type ConversationStatus = 'open' | 'closed' | 'spam' | 'archived'; + +export type ConversationPriority = 'low' | 'medium' | 'high' | 'urgent'; + +export type MessageStatus = 'queued' | 'sent' | 'delivered' | 'failed'; + +export type MessageDirection = 'inbound' | 'outbound'; + +export interface AttachmentInfo { + url: string; + filename: string; + contentType?: string; + size?: number; +} + +export interface EmailAttachmentMeta { + id: string; + filename: string; + contentType: string; + size: number; + storageId?: string; + url?: string; + contentId?: string; +} + +export interface MessageInfo { + id: string; + sender: string; + content: string; + timestamp: string; + isCustomer: boolean; + status: MessageStatus; + /** Epoch ms the delayed send action fires (queued outbound only) — drives + * the composer's "Sending in Ns · Undo" countdown. */ + scheduledSendAt?: number; + /** Delivery failure reason (failed outbound only), e.g. an SMTP error. */ + errorMessage?: string; + attachment?: AttachmentInfo; + attachments?: EmailAttachmentMeta[]; +} + +export interface ContactInfo { + id: string; + name?: string; + email: string; + locale?: string; + source?: string; + created_at: string; +} + +export interface BulkOperationResult { + successCount: number; + failedCount: number; + errors: string[]; +} + +export interface ConversationItem { + _id: string; + _creationTime: number; + organizationId: string; + contactId?: string; + /** Internal member owner (Better Auth userId). Surfaced so the conversation + * header can show the current assignee and gate the admin picker. */ + assigneeUserId?: string; + /** Internal team the conversation is queued to (Better Auth teamId). + * Surfaced so the header can show the team chip and gate the admin + * picker. */ + assigneeTeamId?: string; + externalMessageId?: string; + subject?: string; + status?: ConversationStatus; + priority?: string; + type?: string; + channel?: string; + direction?: MessageDirection; + connectorName?: string; + lastMessageAt?: number; + metadata?: Record; + id: string; + title: string; + description: string; + contact_id: string; + business_id: string; + message_count: number; + unread_count: number; + /** Flat list-row fields for the ConversationList block (single-level item + * map): the contact's display name and the latest message's raw content, + * capped server-side. Optional — absent when there is no named contact / + * no message yet. */ + senderName?: string; + lastMessagePreview?: string; + last_message_at?: string; + last_read_at?: string; + resolved_at?: string; + resolved_by?: string; + created_at: string; + updated_at: string; + contact: ContactInfo; + messages: MessageInfo[]; + pendingApproval?: ApprovalItem | null; +} + +export interface ConversationListResponse { + conversations: ConversationItem[]; + total: number; + page: number; + limit: number; + totalPages: number; +} -export type ConversationStatus = Infer; -export type ConversationPriority = Infer; -export type MessageStatus = Infer; -export type MessageInfo = Infer; -export type ContactInfo = Infer; -export type ConversationItem = Infer; -export type ConversationListResponse = Infer< - typeof conversationListResponseValidator ->; -export type ConversationWithMessages = Infer< - typeof conversationWithMessagesValidator ->; -export type BulkOperationResult = Infer; +export type ConversationWithMessages = ConversationItem; // ============================================================================= -// MANUAL TYPES (no corresponding validator) +// MANUAL TYPES // ============================================================================= export interface CreateConversationArgs { diff --git a/services/platform/convex/conversations/validators.ts b/services/platform/convex/conversations/validators.ts deleted file mode 100644 index cd3e77e265..0000000000 --- a/services/platform/convex/conversations/validators.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Convex validators for conversation operations - * - * Uses native Convex v.* validators to avoid pulling zod into the query bundle. - * Zod schemas for client-side validation live in lib/shared/schemas/conversations.ts. - */ - -import { v } from 'convex/values'; - -import { approvalItemValidator } from '../approvals/validators'; - -export const conversationStatusValidator = v.union( - v.literal('open'), - v.literal('closed'), - v.literal('spam'), - v.literal('archived'), -); - -export const conversationPriorityValidator = v.union( - v.literal('low'), - v.literal('medium'), - v.literal('high'), - v.literal('urgent'), -); - -export const messageStatusValidator = v.union( - v.literal('queued'), - v.literal('sent'), - v.literal('delivered'), - v.literal('failed'), -); - -export const messageDirectionValidator = v.union( - v.literal('inbound'), - v.literal('outbound'), -); - -export const attachmentValidator = v.object({ - url: v.string(), - filename: v.string(), - contentType: v.optional(v.string()), - size: v.optional(v.number()), -}); - -export const emailAttachmentMetaValidator = v.object({ - id: v.string(), - filename: v.string(), - contentType: v.string(), - size: v.number(), - storageId: v.optional(v.string()), - url: v.optional(v.string()), - contentId: v.optional(v.string()), -}); - -export const messageValidator = v.object({ - id: v.string(), - sender: v.string(), - content: v.string(), - timestamp: v.string(), - isCustomer: v.boolean(), - status: messageStatusValidator, - // Epoch ms the delayed send action fires (queued outbound only) — drives - // the composer's "Sending in Ns · Undo" countdown. - scheduledSendAt: v.optional(v.number()), - // Delivery failure reason (failed outbound only), e.g. an SMTP error. - errorMessage: v.optional(v.string()), - attachment: v.optional(attachmentValidator), - attachments: v.optional(v.array(emailAttachmentMetaValidator)), -}); - -export const contactInfoValidator = v.object({ - id: v.string(), - name: v.optional(v.string()), - email: v.string(), - locale: v.optional(v.string()), - source: v.optional(v.string()), - created_at: v.string(), -}); - -export const bulkOperationResultValidator = v.object({ - successCount: v.number(), - failedCount: v.number(), - errors: v.array(v.string()), -}); - -export const conversationItemValidator = v.object({ - _id: v.string(), - _creationTime: v.number(), - organizationId: v.string(), - contactId: v.optional(v.string()), - // Internal member owner (Better Auth userId). Surfaced so the conversation - // header can show the current assignee and gate the admin picker. - assigneeUserId: v.optional(v.string()), - // Internal team the conversation is queued to (Better Auth teamId). Surfaced - // so the header can show the team chip and gate the admin picker. - assigneeTeamId: v.optional(v.string()), - externalMessageId: v.optional(v.string()), - subject: v.optional(v.string()), - status: v.optional(conversationStatusValidator), - priority: v.optional(v.string()), - type: v.optional(v.string()), - channel: v.optional(v.string()), - direction: v.optional(messageDirectionValidator), - connectorName: v.optional(v.string()), - lastMessageAt: v.optional(v.number()), - metadata: v.optional(v.any()), - id: v.string(), - title: v.string(), - description: v.string(), - contact_id: v.string(), - business_id: v.string(), - message_count: v.number(), - unread_count: v.number(), - // Flat list-row fields for the ConversationList block (single-level item - // map): the contact's display name and the latest message's raw content, - // capped server-side. Optional — absent when there is no named contact / - // no message yet. - senderName: v.optional(v.string()), - lastMessagePreview: v.optional(v.string()), - last_message_at: v.optional(v.string()), - last_read_at: v.optional(v.string()), - resolved_at: v.optional(v.string()), - resolved_by: v.optional(v.string()), - created_at: v.string(), - updated_at: v.string(), - contact: contactInfoValidator, - messages: v.array(messageValidator), - pendingApproval: v.optional(v.union(approvalItemValidator, v.null())), -}); - -export const conversationListResponseValidator = v.object({ - conversations: v.array(conversationItemValidator), - total: v.number(), - page: v.number(), - limit: v.number(), - totalPages: v.number(), -}); - -export const conversationDocValidator = v.object({ - _id: v.string(), - _creationTime: v.number(), - organizationId: v.string(), - contactId: v.optional(v.string()), - externalMessageId: v.optional(v.string()), - subject: v.optional(v.string()), - status: v.optional(conversationStatusValidator), - priority: v.optional(v.string()), - type: v.optional(v.string()), - channel: v.optional(v.string()), - direction: v.optional(messageDirectionValidator), - connectorName: v.optional(v.string()), - lastMessageAt: v.optional(v.number()), - metadata: v.optional(v.any()), -}); - -export const conversationWithMessagesValidator = conversationItemValidator; diff --git a/services/platform/convex/documents/access.ts b/services/platform/convex/documents/access.ts index 716beb33aa..474321362b 100644 --- a/services/platform/convex/documents/access.ts +++ b/services/platform/convex/documents/access.ts @@ -16,8 +16,6 @@ * or `canReadDocument` (async, resolves project access for single-doc reads). */ -import { v } from 'convex/values'; - import { AppError } from '../../lib/shared/errors/app-error'; import type { MutationCtx, QueryCtx } from '../lib/ctx'; import { getUserTeamIds } from '../lib/get_user_teams'; @@ -230,6 +228,13 @@ export function assertRecordTrashable(doc: DocumentRecordFields): void { * A caller's document visibility for knowledge RETRIEVAL, as sets rather than * per-row checks — what the corpus access filter * (`lib/knowledge/types.ts` `KnowledgeAccessScope`) consumes. + * + * Declared ONCE. It used to be declared three times — the resolver's returns, + * the re-check's args, and the sandbox bridge's returns — and adding a field + * to the scope meant finding all three; a missed one presented as knowledge + * search having gone quiet. `threadIds` is not here: it belongs to the chat + * lane's request, not to what the resolver derives, and the re-check adds it + * to its own args. */ export interface ResolvedKnowledgeAccess { teamIds: string[]; @@ -251,28 +256,6 @@ export interface ResolvedKnowledgeAccess { userId?: string; } -/** - * The wire shape of {@link ResolvedKnowledgeAccess}, declared ONCE. - * - * It was declared three times — the resolver's returns, the re-check's args, - * and the sandbox bridge's returns — and adding a field to the scope meant - * finding all three. Missing one does not fail politely: a closed returns - * validator THROWS on the unexpected field, and a throwing query reads to the - * caller as "no results", so a widened scope would present as knowledge search - * having gone quiet. - * - * `threadIds` is not here: it belongs to the chat lane's request, not to what - * the resolver derives, and the re-check adds it to its own args. - */ -export const knowledgeAccessScopeValidator = v.object({ - teamIds: v.array(v.string()), - projectIds: v.array(v.string()), - includeHub: v.boolean(), - archivedProjectIds: v.optional(v.array(v.string())), - includeConversationScoped: v.optional(v.boolean()), - userId: v.optional(v.string()), -}); - /** Fail-closed scope: no hub, no teams, no projects — a search sees nothing. */ export const NO_KNOWLEDGE_ACCESS: ResolvedKnowledgeAccess = { teamIds: [], diff --git a/services/platform/convex/enterprise_sso/validators.ts b/services/platform/convex/enterprise_sso/validators.ts deleted file mode 100644 index 3236026aae..0000000000 --- a/services/platform/convex/enterprise_sso/validators.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { v } from 'convex/values'; - -/** - * Convex validators for the unified Enterprise SSO model. Mirrors - * `lib/shared/schemas/enterprise_sso.ts`. Stored configs carry ENCRYPTED - * secrets (`*Encrypted` fields); the read-facing config queries strip them. - */ - -export const platformRoleValidator = v.union( - v.literal('admin'), - v.literal('developer'), - v.literal('editor'), - v.literal('member'), - v.literal('disabled'), -); - -export const ssoProtocolValidator = v.union( - v.literal('oidc'), - v.literal('oauth2'), - v.literal('saml'), -); - -export const roleMappingSourceValidator = v.union( - v.literal('jobTitle'), - v.literal('appRole'), - v.literal('group'), - v.literal('claim'), -); - -export const roleMappingRuleValidator = v.object({ - source: roleMappingSourceValidator, - pattern: v.string(), - targetRole: platformRoleValidator, - claim: v.optional(v.string()), -}); - -export const attributeMappingValidator = v.object({ - email: v.optional(v.string()), - name: v.optional(v.string()), - groups: v.optional(v.string()), -}); - -export const ssoProviderIdValidator = v.union( - v.literal('entra-id'), - v.literal('generic-oidc'), - v.literal('oauth2'), -); - -export const ssoResourceTypeValidator = v.union( - v.literal('User'), - v.literal('Group'), -); diff --git a/services/platform/convex/governance/soft_delete_validators.ts b/services/platform/convex/governance/soft_delete.ts similarity index 80% rename from services/platform/convex/governance/soft_delete_validators.ts rename to services/platform/convex/governance/soft_delete.ts index e810fa0206..aeff8f45ea 100644 --- a/services/platform/convex/governance/soft_delete_validators.ts +++ b/services/platform/convex/governance/soft_delete.ts @@ -1,7 +1,5 @@ -import { v } from 'convex/values'; - /** - * Soft-delete lifecycle states. Mirrors the existing `threadStatusValidator` + * Soft-delete lifecycle states. Mirrors the thread-status * shape (active/trashed/expired/deleted) so retention's two-pass * grace-window machinery is uniform across tables. * @@ -21,13 +19,6 @@ export const SOFT_DELETE_STATUSES = [ export type SoftDeleteStatus = (typeof SOFT_DELETE_STATUSES)[number]; -export const lifecycleStatusValidator = v.union( - v.literal('active'), - v.literal('trashed'), - v.literal('expired'), - v.literal('deleted'), -); - /** * Resource types that participate in the soft-delete + grace + restore * lifecycle. The Trash UI lists rows by these keys, the generic restore @@ -56,7 +47,3 @@ export const SOFT_DELETE_RESOURCE_TYPES = [ export type SoftDeleteResourceType = (typeof SOFT_DELETE_RESOURCE_TYPES)[number]; - -export const softDeleteResourceTypeValidator = v.union( - ...SOFT_DELETE_RESOURCE_TYPES.map((t) => v.literal(t)), -); diff --git a/services/platform/convex/lib/providers/harness_status.ts b/services/platform/convex/lib/providers/harness_status.ts index 45558fd862..ecadfc83ad 100644 --- a/services/platform/convex/lib/providers/harness_status.ts +++ b/services/platform/convex/lib/providers/harness_status.ts @@ -21,8 +21,6 @@ * `'use node'` by necessity — the harness facts and org providers are files. */ -import { v, type Infer } from 'convex/values'; - import { buildHarnessTable, resolveExecution, @@ -32,32 +30,28 @@ import type { HarnessDefinition, ModelCatalogEntry, } from '../../../lib/shared/schemas/providers'; -const harnessManagedStatusValidator = v.union( - v.object({ - available: v.literal(true), - /** How many directly-served models the managed lane offers this harness. */ - modelCount: v.number(), - /** The model a turn runs when the composer sends no explicit pick. */ - defaultModelId: v.string(), - }), - v.object({ - available: v.literal(false), - reason: v.literal('no-direct-credential'), - }), -); -const harnessStatusValidator = v.object({ - slug: v.string(), - label: v.string(), - managed: harnessManagedStatusValidator, +export type HarnessManagedStatus = + | { + available: true; + /** How many directly-served models the managed lane offers this harness. */ + modelCount: number; + /** The model a turn runs when the composer sends no explicit pick. */ + defaultModelId: string; + } + | { + available: false; + reason: 'no-direct-credential'; + }; + +export interface HarnessStatusEntry { + slug: string; + label: string; + managed: HarnessManagedStatus; /** Vendor subscriptions bound to this harness; `usable: false` marks an * inert binding (the harness cannot accept bring-your-own credentials). */ - subscriptions: v.array( - v.object({ providerSlug: v.string(), usable: v.boolean() }), - ), -}); - -export type HarnessStatusEntry = Infer; + subscriptions: { providerSlug: string; usable: boolean }[]; +} /** A subscription-flavored credential, resolver-shaped. */ type SubscriptionAuth = Extract< diff --git a/services/platform/convex/lib/storage/blob_delete.ts b/services/platform/convex/lib/storage/blob_delete.ts index b0aea525b7..4e8337aa5a 100644 --- a/services/platform/convex/lib/storage/blob_delete.ts +++ b/services/platform/convex/lib/storage/blob_delete.ts @@ -31,14 +31,14 @@ export async function deleteBlobInMutation( ): Promise { const convexId = convexStorageId(ref); if (convexId === null) { - s3Refs.push(String(ref)); + s3Refs.push(ref); return; } try { await ctx.storage.delete(convexId); } catch (err) { console.warn( - `[${label}] storage.delete failed for ${String(ref)}:`, + `[${label}] storage.delete failed for ${ref}:`, err instanceof Error ? err.message : err, ); } diff --git a/services/platform/convex/lib/storage/blob_ref.ts b/services/platform/convex/lib/storage/blob_ref.ts index 231c6071a2..116ae64add 100644 --- a/services/platform/convex/lib/storage/blob_ref.ts +++ b/services/platform/convex/lib/storage/blob_ref.ts @@ -1,63 +1,45 @@ /** - * Pure blob-reference encoding — V8-safe (NO `node:*`, NO `fetch`), so Convex - * schema files, queries, and mutations can import the validator + parser. The - * actual S3 I/O lives in the `'use node'` sibling `blob_access.ts`. + * Pure blob-reference encoding — V8-safe (NO `node:*`, NO `fetch`), so any + * module can import the parser. The actual S3 I/O lives in the `blob_access.ts` + * sibling. * - * A stored blob reference is a STRING that is EITHER a Convex `_storage` id - * (unchanged — the deployment default) OR `s3:` (the bytes live in - * the org's own bucket). This is the ONLY module that knows the encoding. + * A stored blob reference is a STRING that is EITHER a legacy `_storage` id + * (the retired Convex deployment default) OR `s3:` (the bytes live + * in the org's own bucket). This is the ONLY module that knows the encoding. */ -import { v, type GenericId } from 'convex/values'; - -// Use `GenericId` from `convex/values` — NOT `Id` from `_generated/dataModel`. -// This module's `blobRefValidator` is imported BY the schema (documents / -// fileMetadata), and `dataModel`'s `Id` is derived FROM the schema, so importing -// `Id` here would close a schema↔dataModel type cycle that poisons every -// query/mutation ctx type (TS2719). `GenericId<'_storage'>` is identical to -// `Id<'_storage'>` but schema-independent. -type StorageId = GenericId<'_storage'>; - -/** A stored blob reference: a Convex `_storage` id, or an `s3:`-prefixed key. */ -export type BlobRef = StorageId | string; +/** A stored blob reference: a legacy `_storage` id, or an `s3:`-prefixed key. */ +export type BlobRef = string; const S3_PREFIX = 's3:'; -/** - * Schema validator for a blob-reference field. Widening a legacy - * `v.id('_storage')` field to this union is backward-compatible — every - * existing id value still validates, and new S3 refs land in the string arm. - */ -export const blobRefValidator = v.union(v.id('_storage'), v.string()); - /** Encode an S3 object key as a stored blob reference. */ export function encodeS3Ref(key: string): string { return `${S3_PREFIX}${key}`; } export type ParsedBlobRef = - | { backend: 'convex'; storageId: StorageId } + | { backend: 'convex'; storageId: string } | { backend: 's3'; key: string }; /** * Decode a stored reference. An `s3:`-prefixed string is an S3 key; anything - * else is a Convex storage id (Convex ids never contain a `:`). + * else is a legacy `_storage` id (those ids never contain a `:`). */ export function parseBlobRef(ref: BlobRef): ParsedBlobRef { - if (typeof ref === 'string' && ref.startsWith(S3_PREFIX)) { + if (ref.startsWith(S3_PREFIX)) { return { backend: 's3', key: ref.slice(S3_PREFIX.length) }; } - // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- a non-`s3:` ref is a Convex `_storage` id by construction - return { backend: 'convex', storageId: ref as StorageId }; + return { backend: 'convex', storageId: ref }; } /** True when a stored reference points at the org's S3 bucket (not `_storage`). */ export function isS3Ref(ref: BlobRef): boolean { - return typeof ref === 'string' && ref.startsWith(S3_PREFIX); + return ref.startsWith(S3_PREFIX); } -/** The Convex storage id of a convex-backed ref, or null for an S3 ref. */ -export function convexStorageId(ref: BlobRef): StorageId | null { +/** The legacy storage id of a convex-backed ref, or null for an S3 ref. */ +export function convexStorageId(ref: BlobRef): string | null { const parsed = parseBlobRef(ref); return parsed.backend === 'convex' ? parsed.storageId : null; } @@ -68,12 +50,11 @@ export function convexStorageId(ref: BlobRef): StorageId | null { * `buildObjectKey` always mints `[/]/`, so the * second-to-last segment IS the owning org, regardless of the org-chosen * `prefix` (and of later prefix changes — old keys still carry the slug). - * Blob refs are client-bindable strings (`blobRefValidator` accepts any - * string), so every S3 read / presign / delete MUST refuse a key outside the - * org's namespace — otherwise two orgs sharing one physical bucket (a - * supported config; `prefix` exists for exactly that) could address each - * other's objects by binding a foreign key. Empty segments are rejected - * outright (`a//b` never comes out of `buildObjectKey`). + * Blob refs are client-bindable strings, so every S3 read / presign / delete + * MUST refuse a key outside the org's namespace — otherwise two orgs sharing + * one physical bucket (a supported config; `prefix` exists for exactly that) + * could address each other's objects by binding a foreign key. Empty segments + * are rejected outright (`a//b` never comes out of `buildObjectKey`). */ export function s3KeyBelongsToOrg(key: string, orgSlug: string): boolean { const segments = key.split('/'); diff --git a/services/platform/convex/lib/type_cast_helpers.ts b/services/platform/convex/lib/type_cast_helpers.ts deleted file mode 100644 index 346219ddfd..0000000000 --- a/services/platform/convex/lib/type_cast_helpers.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* oxlint-disable typescript/no-unsafe-type-assertion -- Centralized cast helpers for Convex branded types */ - -/** - * Centralized type cast helpers for Convex branded types. - * - * Convex uses branded types (`Id`, `ConvexJsonRecord`, `ConvexJsonValue`) - * that require casts from plain strings / objects. These helpers concentrate - * the unavoidable `as` casts into a single file so the rest of the codebase - * stays cast-free. - */ - -import type { GenericId } from 'convex/values'; - -import type { - ConvexJsonRecord, - ConvexJsonValue, -} from '../../lib/shared/schemas/utils/json-value'; - -/** Cast a plain string to a typed Convex document ID. */ -export function toId(s: string): GenericId { - return s as GenericId; -} - -/** Cast an array of strings to typed Convex document IDs. */ -export function toIds(arr: string[]): GenericId[] { - return arr as GenericId[]; -} - -/** Cast a record-like value to `ConvexJsonRecord` for Convex storage. */ -export function toConvexJsonRecord(obj: unknown): ConvexJsonRecord { - return obj as ConvexJsonRecord; -} - -/** Cast any value to `ConvexJsonValue` for Convex storage. */ -export function toConvexJsonValue(val: unknown): ConvexJsonValue { - return val as ConvexJsonValue; -} - -/** Cast an array to `ConvexJsonValue[]` for Convex storage. */ -export function toConvexJsonValues(arr: unknown[]): ConvexJsonValue[] { - return arr as ConvexJsonValue[]; -} diff --git a/services/platform/convex/lib/validators/json.ts b/services/platform/convex/lib/validators/json.ts deleted file mode 100644 index 2d659c97f3..0000000000 --- a/services/platform/convex/lib/validators/json.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Zod-free JSON value validators for Convex - * - * These mirror jsonRecordValidator/jsonValueValidator from lib/shared/schemas/utils/json-value.ts - * but without importing zod, keeping query bundles lean. - */ - -import type { Infer } from 'convex/values'; -import { v } from 'convex/values'; - -export const jsonValueValidator = v.any(); - -export const jsonRecordValidator = v.any(); - -export type ConvexJsonValue = Infer; -export type ConvexJsonRecord = Infer; diff --git a/services/platform/convex/node_only/sandbox/session_exec.ts b/services/platform/convex/node_only/sandbox/session_exec.ts index a0d46a0532..d30d6c1f41 100644 --- a/services/platform/convex/node_only/sandbox/session_exec.ts +++ b/services/platform/convex/node_only/sandbox/session_exec.ts @@ -497,7 +497,7 @@ export async function harvestSessionOutput( } files.push({ path: absPath, - storageId: String(storageId), + storageId, size: buf.byteLength, contentType, }); diff --git a/services/platform/convex/notifications/helpers.ts b/services/platform/convex/notifications/helpers.ts index 578db8d1e0..ccc37fb65a 100644 --- a/services/platform/convex/notifications/helpers.ts +++ b/services/platform/convex/notifications/helpers.ts @@ -1,13 +1,11 @@ -import type { Infer } from 'convex/values'; - import type { MutationCtx } from '../lib/ctx'; import { internal } from '../lib/handler_names'; import { isAdmin } from '../lib/rls/helpers/role_helpers'; import type { NOTIFICATION_CATEGORIES, NOTIFICATION_SEVERITIES, - notificationLinkValidator, -} from './schema'; + NotificationLink, +} from './types'; type Category = (typeof NOTIFICATION_CATEGORIES)[number]; type Severity = (typeof NOTIFICATION_SEVERITIES)[number]; @@ -42,7 +40,7 @@ interface WriteNotificationArgs { */ subjectUserId?: string; /** Optional in-app deep-link target for the notification body. */ - link?: Infer; + link?: NotificationLink; } /** diff --git a/services/platform/convex/notifications/schema.ts b/services/platform/convex/notifications/types.ts similarity index 59% rename from services/platform/convex/notifications/schema.ts rename to services/platform/convex/notifications/types.ts index 4e63221d0f..4ead73d1b0 100644 --- a/services/platform/convex/notifications/schema.ts +++ b/services/platform/convex/notifications/types.ts @@ -1,4 +1,3 @@ -import { v } from 'convex/values'; export const NOTIFICATION_CATEGORIES = ['security', 'system'] as const; export const NOTIFICATION_SEVERITIES = ['info', 'warning', 'critical'] as const; @@ -6,16 +5,14 @@ export const NOTIFICATION_SEVERITIES = ['info', 'warning', 'critical'] as const; * Optional in-app deep-link target for a notification. The client maps each * `kind` to a concrete dashboard route (see `notification-target.ts`) so the * stored value stays route-agnostic (survives route refactors, locale-safe). - * Closed union — schema ships one release ahead of new emitters, per the - * Convex closed-union deploy-order constraint. + * Closed union — the client only knows how to route these kinds. */ -export const notificationLinkValidator = v.union( - v.object({ kind: v.literal('agent'), agentSlug: v.string() }), +export type NotificationLink = + | { kind: 'agent'; agentSlug: string } // Optional `logId` deep-links to the specific broken audit row (#1845). Kept // optional so it's data-safe (widened member, no migration) and so findings // without a concrete row — e.g. a config/checkpoint gap — still link to the // audit-log page. - v.object({ kind: v.literal('audit-logs'), logId: v.optional(v.string()) }), - v.object({ kind: v.literal('dsar') }), - v.object({ kind: v.literal('security-monitoring') }), -); + | { kind: 'audit-logs'; logId?: string } + | { kind: 'dsar' } + | { kind: 'security-monitoring' }; diff --git a/services/platform/convex/sandbox/sessions_schema.ts b/services/platform/convex/sandbox/session_constants.ts similarity index 85% rename from services/platform/convex/sandbox/sessions_schema.ts rename to services/platform/convex/sandbox/session_constants.ts index fb4441b1c3..c52d535eba 100644 --- a/services/platform/convex/sandbox/sessions_schema.ts +++ b/services/platform/convex/sandbox/session_constants.ts @@ -1,20 +1,3 @@ -import { v } from 'convex/values'; - -/** - * One entry of an op row's `liveTimeline` — the AI-SDK UI-part shape the run - * views render. Exported so the public op-reading queries (automation agent - * node, task-agent run) project it without re-declaring the shape. - */ -export const sessionOpTimelinePartValidator = v.object({ - type: v.string(), - text: v.optional(v.string()), - state: v.optional(v.string()), - toolCallId: v.optional(v.string()), - input: v.optional(v.any()), - output: v.optional(v.any()), - errorText: v.optional(v.string()), -}); - /** * Deterministic name of the workflow event that wakes a parked sandbox step * waiting on capacity. A parked durable step does `step.awaitEvent({ name })`; diff --git a/services/platform/convex/sandbox/wire.ts b/services/platform/convex/sandbox/wire.ts deleted file mode 100644 index fa51951c90..0000000000 --- a/services/platform/convex/sandbox/wire.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { v } from 'convex/values'; - -// Type-only import of the spawner's harvest output-file shape so the -// compile-time parity guard at the bottom of this file catches any drift -// between the bytes the spawner emits and the shape Convex consumes. -import type { OutputFile as SpawnerOutputFile } from '../../../sandbox/src/types'; -// Type-only imports from the spawner's wire module — purely structural, -// nothing of this lands in the convex runtime bundle. We use these in the -// compile-time parity assertions at the bottom of the file so a literal -// drift on EITHER side fails CI typecheck. Audit finding R2-B3 caught -// that the docstring claimed this guard existed when it didn't. -import type { - sandboxErrorCodeLiterals as SpawnerErrorCodes, - sandboxSessionProfileLiterals as SpawnerSessionProfiles, - sandboxSseEventLiterals as SpawnerSseEvents, -} from '../../../sandbox/src/wire'; - -/** - * Single source of truth for the sandbox runtime's wire protocol on the - * Convex side. Both the audit row (`sandboxExecutions`) and the artifact - * runnable run-state (`artifacts.run*` fields) build their validators from - * the literal arrays exported here — adding or removing a code never - * requires touching multiple schema files. The spawner-side mirror lives - * at `services/sandbox/src/wire.ts`; the bidirectional `extends` checks - * at the bottom of this file keep them from drifting. - * - * Pattern mirrors `services/platform/convex/tts/error_codes.ts`. - */ - -export const sandboxRunStatusLiterals = [ - 'queued', - // Set while pip / npm install is fetching deps. The audit row stays in - // `queued` until the spawner reports a phase event; the artifact row - // mirrors `installing` so the canvas can distinguish "waiting for slot" - // from "downloading torch". The audit-row lifecycle is - // queued → installing → terminal — `running` is never persisted there; - // see the comment on `setRunning` in `internal_mutations.ts`. The literal - // below is retained for read-validation of legacy rows and for the - // artifact-side `runStatus` field (which DOES use `running` to drive the - // canvas spinner). Watchdog reaps queued, installing, and running. - 'installing', - 'running', - 'completed', - 'failed', - 'cancelled', -] as const; - -export type SandboxRunStatus = (typeof sandboxRunStatusLiterals)[number]; - -export const sandboxRunStatusValidator = v.union( - v.literal('queued'), - v.literal('installing'), - // 'running' retained for legacy audit rows pre-refactor and for the - // artifact `runStatus` field; new audit-row writes emit 'installing' only. - v.literal('running'), - v.literal('completed'), - v.literal('failed'), - v.literal('cancelled'), -); - -export const sandboxTerminalStatuses: ReadonlySet = new Set([ - 'completed', - 'failed', - 'cancelled', -]); - -export const sandboxErrorCodeLiterals = [ - 'TIMEOUT', - 'OOM', - 'EGRESS_DENIED', - 'INSTALL_FAILED', - 'PACKAGE_NOT_FOUND', - 'QUOTA_EXCEEDED', - 'RUNTIME_ERROR', - 'SPAWNER_UNAVAILABLE', - 'CANCELLED', - // The action validated the input but rejected it (file missing, - // not in the requested thread, IDOR check failed). Distinct from - // SPAWNER_UNAVAILABLE so the agent's recovery hint is "fix the args", - // not "retry the transient infra". - 'INPUT_REJECTED', - // Output-pipeline error codes (sandbox-wobbly-origami plan §5). Split out - // of the legacy catch-all `HARVEST_FAILED` so the LLM-side recovery hint - // can be specific. See artifact_run_tool.ts for the per-code recovery - // table; the spawner-side mirror is in services/sandbox/src/wire.ts. - 'HARVEST_READ_FAILED', - 'UPLOAD_FAILED', - 'UPLOAD_QUOTA_EXCEEDED', - 'UPLOAD_REPORT_FAILED', - // Pre-stage attestation failure: the spawner reported `priorStage.skipped` - // entries for files the platform expected to inject into - // `/agent/output/` before user code ran. Abort BEFORE the container - // starts so the LLM cannot run against a corrupted workspace. The - // `errorMessage` payload carries a JSON `{skipped: [{name, reason}], ...}` - // breakdown so the LLM can decide whether to retry with - // `inputs.from_run: ` or surface the issue. - 'PRE_STAGE_FAILED', - // Output-pipeline completeness gate: `uploadStats.failures` came back - // non-empty (either an upload POST or the EP2 record-uploaded callback - // dropped). The bytes that made it to `_storage` are cleaned via the - // existing `uploadedStorageIds[]` rollback; the run is failed so the - // LLM doesn't trust a partial workspace state. Distinct from the - // per-failure codes above because this is the action-side decision - // that "any failure → fatal", not a single transport-layer cause. - 'UPLOAD_INCOMPLETE', - // Session-exec error codes (sessions plan, milestone A). SESSION_LOST: the - // session container/Pod (or its runnerd) died mid-exec — the workspace may - // survive, so the caller checks GET /v1/sessions/:id to decide retry vs - // recreate. INVALID_CWD: an exec cwd failed runnerd's realpath-under- - // /agent check. Spawner-side mirror in services/sandbox/src/wire.ts. - 'SESSION_LOST', - 'INVALID_CWD', -] as const; - -export type SandboxErrorCode = (typeof sandboxErrorCodeLiterals)[number]; - -export const sandboxErrorCodeValidator = v.union( - v.literal('TIMEOUT'), - v.literal('OOM'), - v.literal('EGRESS_DENIED'), - v.literal('INSTALL_FAILED'), - v.literal('PACKAGE_NOT_FOUND'), - v.literal('QUOTA_EXCEEDED'), - v.literal('RUNTIME_ERROR'), - v.literal('SPAWNER_UNAVAILABLE'), - v.literal('CANCELLED'), - v.literal('INPUT_REJECTED'), - v.literal('HARVEST_READ_FAILED'), - v.literal('UPLOAD_FAILED'), - v.literal('UPLOAD_QUOTA_EXCEEDED'), - v.literal('UPLOAD_REPORT_FAILED'), - v.literal('PRE_STAGE_FAILED'), - v.literal('UPLOAD_INCOMPLETE'), - v.literal('SESSION_LOST'), - v.literal('INVALID_CWD'), -); - -/** - * SSE event-type vocabulary emitted by the spawner's `POST /v1/execute`. - * Mirror of `services/sandbox/src/wire.ts:sandboxSseEventLiterals`. The - * compile-time `Equal<>` parity check below catches drift in either - * direction. Adding a new event type requires updating both wire files - * AND the `spawner_client.ts` SSE-parser switch (the parser is the actual - * consumer; this constant is the documentation contract). - */ -export const sandboxSseEventLiterals = [ - 'phase', - 'stdout', - 'stderr', - 'result', - 'error', -] as const; - -export type SandboxSseEvent = (typeof sandboxSseEventLiterals)[number]; - -/** - * Session resource-profile validator (persistent sessions). Mirror of - * `services/sandbox/src/wire.ts:sandboxSessionProfileLiterals`. `default` - * mirrors the one-shot caps (uid 65534); `agent` is the external-agent shape - * (uid 10001, larger caps). Used by the `sandboxSessions` table + the - * platform-side session client. - */ -export const sandboxSessionProfileValidator = v.union( - v.literal('default'), - v.literal('agent'), -); - -export type SandboxSessionProfile = 'default' | 'agent'; - -/** - * Structured progress payload persisted on the artifact row alongside the - * phase. Replaces the legacy `runProgress` string field — keys come from - * a stable enum and locale-specific text is composed in the UI via the - * `chat.runnable.progress.*` message keys, so the server never writes - * English literals that the UI cannot translate. - */ -export const sandboxRunProgressLiterals = [ - 'queued', - 'preparing', - 'installingPackage', - 'installing', - 'running', -] as const; - -export type SandboxRunProgressKind = - (typeof sandboxRunProgressLiterals)[number]; - -export const sandboxRunProgressValidator = v.object({ - kind: v.union( - v.literal('queued'), - v.literal('preparing'), - v.literal('installingPackage'), - v.literal('installing'), - v.literal('running'), - ), - // Populated only for `installingPackage` — `{ package: 'python-pptx', - // version: '1.0.2' }`. Empty / omitted for the other kinds. - package: v.optional(v.string()), - version: v.optional(v.string()), -}); - -/** - * Output-file shape used by both `sandboxExecutions.outputFiles` (audit - * row, no denormalized storageId) and `artifacts.runOutputFiles` (canvas - * fast-path, denormalized storageId). `storageId` is optional so the same - * validator covers both call sites; callers that need it must check. - */ -export const sandboxOutputFileValidator = v.object({ - name: v.string(), - size: v.number(), - contentType: v.string(), - fileMetadataId: v.id('fileMetadata'), - storageId: v.optional(v.id('_storage')), - // Optional so historical rows (and the audit-row projection that doesn't - // need it) continue to validate. New harvests always populate sha256 — - // it's set by the spawner during `harvestOutputDir` and used for the - // cumulative manifest (artifactOutputs) + pre-stage attestation. - sha256: v.optional(v.string()), -}); - -export interface SandboxOutputFile { - name: string; - size: number; - contentType: string; - fileMetadataId: string; - storageId?: string; - sha256?: string; -} - -/** - * Spawner-emitted harvest output-file shape. Always populated by the - * spawner's `harvestOutputDir`; `storageId` and `sha256` are required here - * because the spawner has just uploaded the bytes and computed the hash. - * Convex transforms this into {@link SandboxOutputFile} when persisting to - * the audit row (allocates `fileMetadataId`; `storageId` / `sha256` flow - * through verbatim). - * - * The compile-time parity guard at the bottom of this file ensures this - * stays byte-identical to `services/sandbox/src/types.ts:OutputFile`. If - * spawner adds or removes a field on its `OutputFile`, the typecheck fails - * here, forcing a coordinated update before merge. - */ -export interface HarvestOutputFile { - name: string; - storageId: string; - size: number; - contentType: string; - sha256: string; -} - -export const sandboxTruncatedValidator = v.object({ - stdout: v.boolean(), - stderr: v.boolean(), - files: v.number(), -}); - -// --------------------------------------------------------------------------- -// Spawner ↔ Convex literal parity (audit finding R2-B3) -// --------------------------------------------------------------------------- -// Compile-time double-extension checks: each literal-set on this side -// must be both a superset AND a subset of the spawner-side set (i.e. -// equal). Adding a literal on only one side fails CI typecheck with a -// clear error pointing at the assigning line, before the divergence -// ever ships. Purely type-level — no runtime cost. -// -// `Equal` returns `true` iff the two unions -// match. If the spawner has an extra literal, ConvexSide ⊊ SpawnerSide -// breaks the second clause. If Convex has an extra, the first clause -// breaks. The error object is a fake type whose key surfaces a -// readable diagnostic next to the failing literal-array name. -type Equal = [A] extends [B] - ? [B] extends [A] - ? true - : { - __wireDrift: 'Spawner has literal(s) missing from Convex side — add them here too'; - } - : { - __wireDrift: 'Convex has literal(s) missing from spawner side — add them in services/sandbox/src/wire.ts'; - }; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const _errorCodeParity: Equal< - (typeof sandboxErrorCodeLiterals)[number], - (typeof SpawnerErrorCodes)[number] -> = true; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const _sseEventParity: Equal< - (typeof sandboxSseEventLiterals)[number], - (typeof SpawnerSseEvents)[number] -> = true; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const _sessionProfileParity: Equal< - SandboxSessionProfile, - (typeof SpawnerSessionProfiles)[number] -> = true; - -// Harvest output-file shape parity. Both sides declare: -// { name, storageId, size, contentType, sha256 } -// — all required, all primitive strings/numbers. If the spawner side adds -// or removes a field on its `OutputFile`, the Equal<> below fails here -// with a clear diagnostic, forcing a coordinated update before merge. -// (The audit-row validator `sandboxOutputFileValidator` keeps storageId/ -// sha256 optional indefinitely so legacy rows pass — see plan §A.) -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const _harvestOutputFileParity: Equal = - true; diff --git a/services/platform/convex/sandbox/workspace_access.ts b/services/platform/convex/sandbox/workspace_access.ts index 329f8eb3c9..d8042b8c98 100644 --- a/services/platform/convex/sandbox/workspace_access.ts +++ b/services/platform/convex/sandbox/workspace_access.ts @@ -8,25 +8,6 @@ * (`lib/rls/helpers/agent_read_access.ts`). */ -import { v } from 'convex/values'; - -/** - * The wire spelling of {@link AgentReadSubject}. Convex needs literal - * validators, so this list cannot be generated from the const array — a test - * asserts the two agree instead, because a subject present in one and missing - * from the other is an argument-validation error at dispatch, not a type error - * at build. - */ -export const agentReadSubjectValidator = v.union( - v.literal('documents'), - v.literal('contacts'), - v.literal('products'), - v.literal('websites'), - v.literal('tasks'), - v.literal('projects'), - v.literal('conversations'), -); - /** * The subjects the SESSION-BINDING gate arbitrates — a strict subset of * {@link AgentReadSubject}, and a different question. diff --git a/services/platform/convex/skills/file_actions.ts b/services/platform/convex/skills/file_actions.ts index 1622bcac1b..7e4b833a54 100644 --- a/services/platform/convex/skills/file_actions.ts +++ b/services/platform/convex/skills/file_actions.ts @@ -40,7 +40,7 @@ import { type SkillDocumentView, type SkillListingView, type SkillSummaryView, -} from './validators'; +} from './views'; /** `skills//SKILL.md` — the path an operator sees, org-tree relative. */ function relativeSkillPath(slug: string): string { diff --git a/services/platform/convex/skills/validators.ts b/services/platform/convex/skills/validators.ts deleted file mode 100644 index 247a6fd1ee..0000000000 --- a/services/platform/convex/skills/validators.ts +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Wire shapes shared by the `skills` domain's public actions and the - * `'use node'` file actions behind them. - * - * Kept in its own module (no `'use node'`, no filesystem) so both layers - * import the same validators without either pulling the other's runtime in. - * Nothing here describes an executable: a skill crosses the wire as metadata - * plus markdown, which is all it ever is. - */ - -import { v } from 'convex/values'; - -import type { SkillVisibility } from '../../lib/shared/schemas/skills'; -import type { SkillViewer } from '../../lib/skills/visibility'; - -/** - * `private | team | org` at the wire boundary. The frontmatter schema's - * `SKILL_VISIBILITIES` stays the source of truth for the set; the type - * parameters here fail the build if a literal ever stops belonging to it. - * `private` is retired for new skills but stays on the wire: pre-existing - * private bundles still list and round-trip for their owner. - */ -export const skillVisibilityValidator = v.union( - v.literal('private'), - v.literal('team'), - v.literal('org'), -); - -/** The fields every skill view carries. */ -const skillSummaryFields = { - slug: v.string(), - description: v.string(), - visibility: skillVisibilityValidator, - /** Team ids a `team` skill is shared with; absent otherwise. */ - teams: v.optional(v.array(v.string())), - owner: v.optional(v.string()), - icon: v.optional(v.string()), - labels: v.optional(v.array(v.string())), - /** True when the model must not reach for the skill on its own. */ - disableModelInvocation: v.optional(v.boolean()), - /** Whether the asking member may change this bundle. */ - canEdit: v.boolean(), -}; - -export const skillSummaryValidator = v.object(skillSummaryFields); - -/** One bundle file named without its bytes, for the detail file tree. */ -export const skillFileEntryValidator = v.object({ - path: v.string(), - size: v.number(), -}); - -/** A skill with its markdown body — the knowledge an agent expands. */ -export const skillDocumentValidator = v.object({ - ...skillSummaryFields, - body: v.string(), - /** Every file of the bundle (including `SKILL.md`), sorted by path. */ - files: v.array(skillFileEntryValidator), -}); - -/** - * One file of a bundle as staged into a sandbox session: its bundle-relative - * POSIX path plus base64 bytes. `SKILL.md` travels verbatim alongside its - * assets — the staged copy is the bundle exactly as the org's tree has it. - */ -export const skillBundleFileValidator = v.object({ - path: v.string(), - contentBase64: v.string(), -}); - -export const skillBundleValidator = v.object({ - files: v.array(skillBundleFileValidator), -}); - -/** - * A bundle that failed to load. `path` is relative to the org's config tree - * so an operator can find the file without the server's absolute layout - * being handed to a browser. - */ -export const skillLoadFailureValidator = v.object({ - slug: v.string(), - path: v.string(), - message: v.string(), -}); - -export const skillListingValidator = v.object({ - skills: v.array(skillSummaryValidator), - failures: v.array(skillLoadFailureValidator), -}); - -/** Editable fields of a skill. Everything else in the file round-trips. */ -export const skillEditArgs = { - description: v.string(), - body: v.string(), - /** - * Absent keeps an existing skill's current visibility and makes a new one - * `org`. `private` is retired: the save handler refuses it unless the - * skill already carries it (an owner editing a pre-existing private - * bundle keeps it as it is). - */ - visibility: v.optional(skillVisibilityValidator), - /** - * Team ids for a `team` skill. Absent keeps an existing skill's teams; - * the save handler rejects a `team` skill that would end up with none and - * strips the list when visibility resolves to anything else. - */ - teams: v.optional(v.array(v.string())), - icon: v.optional(v.string()), - labels: v.optional(v.array(v.string())), -}; - -/** - * The identity a skill is read for, mirroring `lib/skills/visibility.ts`'s - * `SkillViewer`: a member (their own teams + admin bit), a project (its - * teams), or org-level machinery. The type parameters fail the build if the - * wire shape drifts from the pure predicate's. - */ -export const skillViewerValidator = v.union( - v.object({ - kind: v.literal('user'), - userId: v.string(), - teamIds: v.array(v.string()), - isOrgAdmin: v.boolean(), - }), - v.object({ - kind: v.literal('project'), - teamIds: v.array(v.string()), - }), - v.object({ - kind: v.literal('org'), - }), -); - -export interface SkillSummaryView { - slug: string; - description: string; - visibility: SkillVisibility; - teams?: string[]; - owner?: string; - icon?: string; - labels?: string[]; - disableModelInvocation?: boolean; - canEdit: boolean; -} - -export interface SkillFileEntryView { - path: string; - size: number; -} - -export interface SkillDocumentView extends SkillSummaryView { - body: string; - files: SkillFileEntryView[]; -} - -export interface SkillBundleFileView { - path: string; - contentBase64: string; -} - -export interface SkillBundleView { - files: SkillBundleFileView[]; -} - -export interface SkillLoadFailureView { - slug: string; - path: string; - message: string; -} - -export interface SkillListingView { - skills: SkillSummaryView[]; - failures: SkillLoadFailureView[]; -} diff --git a/services/platform/convex/skills/views.ts b/services/platform/convex/skills/views.ts new file mode 100644 index 0000000000..b8f8e6678d --- /dev/null +++ b/services/platform/convex/skills/views.ts @@ -0,0 +1,76 @@ +/** + * Wire shapes shared by the `skills` domain's routes and the file actions + * behind them. + * + * Kept in its own module (no filesystem) so both layers import the same + * shapes without either pulling the other's runtime in. Nothing here + * describes an executable: a skill crosses the wire as metadata plus + * markdown, which is all it ever is. + */ + +import type { SkillVisibility } from '../../lib/shared/schemas/skills'; + +/** The fields every skill view carries. */ +export interface SkillSummaryView { + slug: string; + description: string; + /** + * `private | team | org`. The frontmatter schema's `SKILL_VISIBILITIES` + * stays the source of truth for the set. `private` is retired for new + * skills but stays on the wire: pre-existing private bundles still list + * and round-trip for their owner. + */ + visibility: SkillVisibility; + /** Team ids a `team` skill is shared with; absent otherwise. */ + teams?: string[]; + owner?: string; + icon?: string; + labels?: string[]; + /** True when the model must not reach for the skill on its own. */ + disableModelInvocation?: boolean; + /** Whether the asking member may change this bundle. */ + canEdit: boolean; +} + +/** One bundle file named without its bytes, for the detail file tree. */ +export interface SkillFileEntryView { + path: string; + size: number; +} + +/** A skill with its markdown body — the knowledge an agent expands. */ +export interface SkillDocumentView extends SkillSummaryView { + body: string; + /** Every file of the bundle (including `SKILL.md`), sorted by path. */ + files: SkillFileEntryView[]; +} + +/** + * One file of a bundle as staged into a sandbox session: its bundle-relative + * POSIX path plus base64 bytes. `SKILL.md` travels verbatim alongside its + * assets — the staged copy is the bundle exactly as the org's tree has it. + */ +export interface SkillBundleFileView { + path: string; + contentBase64: string; +} + +export interface SkillBundleView { + files: SkillBundleFileView[]; +} + +/** + * A bundle that failed to load. `path` is relative to the org's config tree + * so an operator can find the file without the server's absolute layout + * being handed to a browser. + */ +export interface SkillLoadFailureView { + slug: string; + path: string; + message: string; +} + +export interface SkillListingView { + skills: SkillSummaryView[]; + failures: SkillLoadFailureView[]; +} diff --git a/services/platform/convex/tasks/access.ts b/services/platform/convex/tasks/access.ts index 4910bdfda7..cd039234d1 100644 --- a/services/platform/convex/tasks/access.ts +++ b/services/platform/convex/tasks/access.ts @@ -15,12 +15,10 @@ export { type ProjectAccessResult, } from '../projects/access'; -import { type Infer } from 'convex/values'; - import { AppError } from '../../lib/shared/errors/app-error'; -import type { taskAssigneeTypeValidator } from './schema'; +import type { TaskAssigneeType } from './types'; -type TaskActorType = Infer; +type TaskActorType = TaskAssigneeType; /** Shape of a task as far as claim/assign logic cares (DB-agnostic). */ interface TaskAssignableInput { diff --git a/services/platform/convex/tasks/schema.ts b/services/platform/convex/tasks/schema.ts deleted file mode 100644 index ffff01955d..0000000000 --- a/services/platform/convex/tasks/schema.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { v } from 'convex/values'; - -import { blobRefValidator } from '../lib/storage/blob_ref'; - -/** - * Tasks feature schema. - * - * A task is a persistent unit of work that lives inside a {@link projectsTable} - * project and is worked by humans AND AI agents on a shared board. A task's - * access control is *inherited* from its parent project — see - * `tasks/access.ts`, which delegates to `projects/access.ts::checkProjectAccess`. - * There is no task-level ACL. - * - * Polymorphic single assignee (locked product decision): a task is assigned to - * exactly one actor — a human user, an AI agent, or an automation (the - * ownership signal the task board's status choreography arbitrates on). - * `assigneeType` + `assigneeId` are set/cleared together (invariant enforced - * in the mutation layer, mirroring the `projects` teamId/projectId - * mutual-exclusivity style). `assigneeId` is a `string` — not a typed Id — - * because it polymorphically holds a Better Auth userId, an agent slug, or an - * automation store name. - * - * Board ordering uses a lexicographic fractional `rank` (LexoRank-style) so a - * drag-reorder is an O(1) "insert between neighbours" write rather than a - * whole-column renumber. See `tasks/rank.ts`. - * - * Soft-delete via `archivedAt` (mirrors `projects.archivedAt`). Hard delete is - * admin-only and approval-gated for agents. - */ - -export const taskStatusValidator = v.union( - v.literal('backlog'), - v.literal('todo'), - v.literal('in_progress'), - v.literal('in_review'), - v.literal('done'), - v.literal('cancelled'), -); - -export const taskPriorityValidator = v.union( - v.literal('p0'), - v.literal('p1'), - v.literal('p2'), - v.literal('p3'), -); - -/** - * Polymorphic actor type for task ATTRIBUTION — comments, mentions, and - * activity actors are authored by a human (`user`) or an AI agent (`agent`). - * Distinct from the governance `auditLogActorTypeValidator` (which models - * user/system/api/workflow) and from {@link taskAssigneeTypeValidator}, the - * worker trichotomy. - */ -export const taskActorTypeValidator = v.union( - v.literal('user'), - v.literal('agent'), -); - -/** - * What an `@mention` can resolve TO — a superset of the author types above: - * besides a human or an agent, a comment can mention an AUTOMATION (by store - * name), which is how the task surface asks the owning automation to run - * (`triggerMentionedTaskAutomation`). Automations never AUTHOR comments under - * this type — workflow comments post as `agent` — so authorship keeps the - * narrower validator. - */ -export const taskMentionTypeValidator = v.union( - v.literal('user'), - v.literal('agent'), - v.literal('automation'), -); - -/** - * The WORKER a task belongs to — exactly one of three classes: a human - * (`user`), an AI agent (`agent`), or an automation (`app` — `assigneeId` - * then holds the automation's store name, and the board's status verbs run - * its workflow). - */ -export const taskAssigneeTypeValidator = v.union( - v.literal('user'), - v.literal('agent'), - v.literal('app'), -); - -/** - * Creator attribution type for a task. Superset of `taskActorTypeValidator`: - * besides a human (`user`) or an AI agent (`agent`), a task can be provisioned - * by an installed `app` (e.g. the issue-desk app turning a GitHub issue into a - * task) — in which case `createdBy` holds the app slug. This is the ownership - * signal generic task automation arbitrates on: a task with `createdByType: - * 'app'` is driven by that app's own workflow, so the generic loops bail. Kept - * the same literals as `taskActorTypeValidator`, but this one records - * PROVENANCE (who created it) — ownership lives on the assignee. Write-once - * at creation. - */ -export const taskCreatorTypeValidator = v.union( - v.literal('user'), - v.literal('agent'), - v.literal('app'), -); - -/** - * A single image/document attached to a task. Stored SELF-DESCRIBED (name + - * MIME + size alongside the storage id) so the board/detail render without a - * join back to `fileMetadata` — mirroring how chat messages embed their - * attachments. `fileId` is a blob REFERENCE (a Convex `_storage` id or an - * `s3:` ref for a BYO-bucket org); the URL is resolved at render time - * (`getFileUrl`, backend-aware) and the delete cascade routes through - * `deleteStorageWithMetadata` (also backend-aware). The list is bounded by - * `TASK_MAX_ATTACHMENTS` and each `fileType` is validated against - * `TASK_UPLOAD_ALLOWED_TYPES` in the mutation layer (`validateTaskAttachments`). - */ -export const taskAttachmentValidator = v.object({ - fileId: blobRefValidator, - fileName: v.string(), - fileType: v.string(), - fileSize: v.number(), -}); - -/** - * One agent-produced deliverable on the task — the harvested `/agent/output` - * files of the task's agent runs. Self-described like an attachment, plus the - * run that produced it. Merged by `fileName`: a rerun producing the same name - * REPLACES the entry (and its blob), so the task always shows the latest - * deliverable set instead of accumulating stale copies. - */ -export const taskOutputValidator = v.object({ - fileId: blobRefValidator, - fileName: v.string(), - fileType: v.string(), - fileSize: v.number(), - producedAt: v.number(), - runId: v.id('projectAgentRuns'), -}); - -/** - * The `comment` object embedded in `comment.created` / `comment.mentioned` - * automation events. Task comments now live in the message store (no - * `taskComments` doc to attach), so this object is RECONSTRUCTED at emit time. - * Its shape is load-bearing for the task-ops pack: `react-to-mention-in-task` - * reads `input.comment.body`, and `comment.*` event filters resolve - * `comment.projectId` by dot-notation — keep both fields. Typing the - * reconstruction here fails the build if an emit site drifts from this shape. - */ -export interface CommentEventComment { - body: string; - projectId: string; - taskId: string; - mentions: Array<{ type: 'user' | 'agent' | 'automation'; id: string }>; -} - -/** Optional workflow attribution on a task-activity row (workflow-engine writes). */ -export const taskActivityContextValidator = v.object({ - workflowSlug: v.optional(v.string()), - wfExecutionId: v.optional(v.id('wfExecutions')), -}); - -/** Passed into agent internal mutations when the workflow sentinel is the actor. */ -export const taskActivityAttributionValidator = v.object({ - workflowSlug: v.optional(v.string()), - wfExecutionId: v.optional(v.id('wfExecutions')), -}); - -export const boardViewScopeValidator = v.union( - v.literal('personal'), - v.literal('shared'), -); - -export const boardViewTypeValidator = v.union( - v.literal('board'), - v.literal('table'), - v.literal('timeline'), -); - -export const boardViewFiltersValidator = v.object({ - statuses: v.optional(v.array(taskStatusValidator)), - priorities: v.optional(v.array(taskPriorityValidator)), - // Label *names* (not ids) — unused in the live filter UI; kept as strings so - // saved views stay readable across renames without a dead-code id migration. - labels: v.optional(v.array(v.string())), - assigneeIds: v.optional(v.array(v.string())), - search: v.optional(v.string()), -}); - -export const projectAgentRunStatusValidator = v.union( - v.literal('queued'), - v.literal('running'), - v.literal('settled'), - v.literal('failed'), - v.literal('cancelled'), -); diff --git a/services/platform/convex/tasks/types.ts b/services/platform/convex/tasks/types.ts new file mode 100644 index 0000000000..1e67e59b41 --- /dev/null +++ b/services/platform/convex/tasks/types.ts @@ -0,0 +1,23 @@ +/** + * The WORKER a task belongs to — exactly one of three classes: a human + * (`user`), an AI agent (`agent`), or an automation (`app` — `assigneeId` + * then holds the automation's store name, and the board's status verbs run + * its workflow). + */ +export type TaskAssigneeType = 'user' | 'agent' | 'app'; + +/** + * The `comment` object embedded in `comment.created` / `comment.mentioned` + * automation events. Task comments live in the message store (no comment doc + * to attach), so this object is RECONSTRUCTED at emit time. Its shape is + * load-bearing for the task-ops pack: `react-to-mention-in-task` reads + * `input.comment.body`, and `comment.*` event filters resolve + * `comment.projectId` by dot-notation — keep both fields. Typing the + * reconstruction fails the build if an emit site drifts from this shape. + */ +export interface CommentEventComment { + body: string; + projectId: string; + taskId: string; + mentions: Array<{ type: 'user' | 'agent' | 'automation'; id: string }>; +} diff --git a/services/platform/convex/websites/types.ts b/services/platform/convex/websites/types.ts index e88bb96787..0ecf5c3d75 100644 --- a/services/platform/convex/websites/types.ts +++ b/services/platform/convex/websites/types.ts @@ -2,22 +2,60 @@ * Type definitions for website operations */ -import type { Infer } from 'convex/values'; - import type { Id } from '../lib/rows'; -import type { - websiteKindValidator, - websiteStatusValidator, - websiteValidator, -} from './validators'; -// ============================================================================= -// INFERRED TYPES (from validators) -// ============================================================================= +export type WebsiteStatus = + | 'idle' + | 'scanning' + | 'active' + | 'error' + | 'deleting'; + +/** What a websites row IS: a crawled site (pages discovered via + * robots/sitemaps/links) or a curated list of URLs fetched verbatim. Absent + * on rows that predate the distinction — read absent as 'site'. */ +export type WebsiteKind = 'site' | 'list'; + +/** + * The allowed scan-interval cadences. This is the single source of truth for + * every write path (REST, the agent write tool, and the website routes) — + * `scanIntervalToSeconds` maps exactly these values, so an unrecognized value + * would silently fall back to the 6h default and get crawled at the wrong rate. + */ +export const SCAN_INTERVAL_VALUES = [ + '60m', + '6h', + '12h', + '1d', + '5d', + '7d', + '30d', +] as const; + +export type ScanInterval = (typeof SCAN_INTERVAL_VALUES)[number]; + +export function isValidScanInterval(value: unknown): value is ScanInterval { + return ( + typeof value === 'string' && + (SCAN_INTERVAL_VALUES as readonly string[]).includes(value) + ); +} -export type WebsiteStatus = Infer; -export type WebsiteKind = Infer; -export type Website = Infer; +export interface Website { + _id: string; + _creationTime: number; + organizationId: string; + domain: string; + kind?: WebsiteKind; + title?: string; + description?: string; + scanInterval: string; + lastScannedAt?: number; + status?: WebsiteStatus; + pageCount?: number; + crawledPageCount?: number; + metadata?: Record; +} // ============================================================================= // MANUAL TYPES (no corresponding validator) diff --git a/services/platform/convex/websites/validators.ts b/services/platform/convex/websites/validators.ts deleted file mode 100644 index 0d3f67ead4..0000000000 --- a/services/platform/convex/websites/validators.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Convex validators for website operations - */ - -import { v } from 'convex/values'; - -import { jsonRecordValidator } from '../lib/validators/json'; - -export const websiteStatusValidator = v.union( - v.literal('idle'), - v.literal('scanning'), - v.literal('active'), - v.literal('error'), - v.literal('deleting'), -); - -/** What a websites row IS: a crawled site (pages discovered via - * robots/sitemaps/links) or a curated list of URLs fetched verbatim. Absent - * on rows that predate the distinction — read absent as 'site'. */ -export const websiteKindValidator = v.union( - v.literal('site'), - v.literal('list'), -); - -/** - * The allowed scan-interval cadences. This is the single source of truth for - * every write path (REST, the agent write tool, and the Convex actions) — - * `scanIntervalToSeconds` maps exactly these values, so an unrecognized value - * would silently fall back to the 6h default and get crawled at the wrong rate. - */ -export const SCAN_INTERVAL_VALUES = [ - '60m', - '6h', - '12h', - '1d', - '5d', - '7d', - '30d', -] as const; - -export type ScanInterval = (typeof SCAN_INTERVAL_VALUES)[number]; - -export function isValidScanInterval(value: unknown): value is ScanInterval { - return ( - typeof value === 'string' && - (SCAN_INTERVAL_VALUES as readonly string[]).includes(value) - ); -} - -export const websiteValidator = v.object({ - _id: v.string(), - _creationTime: v.number(), - organizationId: v.string(), - domain: v.string(), - kind: v.optional(websiteKindValidator), - title: v.optional(v.string()), - description: v.optional(v.string()), - scanInterval: v.string(), - lastScannedAt: v.optional(v.number()), - status: v.optional(websiteStatusValidator), - pageCount: v.optional(v.number()), - crawledPageCount: v.optional(v.number()), - metadata: v.optional(jsonRecordValidator), -}); diff --git a/services/platform/lib/harnesses/timeline.ts b/services/platform/lib/harnesses/timeline.ts index d927a86aa6..eb4f3c373c 100644 --- a/services/platform/lib/harnesses/timeline.ts +++ b/services/platform/lib/harnesses/timeline.ts @@ -21,7 +21,7 @@ */ /** One entry of a turn's live transcript, in the AI-SDK UI-part shape the - * run views render (`sessionOpTimelinePartValidator` is its runtime twin). */ + * run views render — the shape of an op row's `liveTimeline` entries. */ export interface TimelinePart { type: string; text?: string; diff --git a/services/platform/lib/shared/schemas/utils/json-value.ts b/services/platform/lib/shared/schemas/utils/json-value.ts deleted file mode 100644 index 033e9ee1c9..0000000000 --- a/services/platform/lib/shared/schemas/utils/json-value.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { v, type Infer } from 'convex/values'; - -export const jsonValueValidator = v.any(); - -export const jsonRecordValidator = v.any(); - -// Convex-compatible types (use these instead of JsonValue/JsonRecord when passing to Convex functions) -export type ConvexJsonValue = Infer; -export type ConvexJsonRecord = Infer; From 99a34cfec0bfa61e84697ceeaf22366c33c10634 Mon Sep 17 00:00:00 2001 From: larryro <371767072@qq.com> Date: Tue, 1 Sep 2026 10:33:23 +0800 Subject: [PATCH 2/5] refactor(platform): move the ported domain logic to backend/core --- .github/workflows/checks.yml | 2 +- .github/workflows/e2e.yml | 12 ++- compose.web.yml | 2 +- compose.yml | 4 +- .../ui/src/i18n/tests/checks/usage-missing.ts | 2 +- packages/ui/src/i18n/tests/config.ts | 2 +- packages/ui/src/i18n/tests/usage.ts | 4 +- services/platform/.oxlintrc.json | 13 +-- services/platform/Dockerfile | 84 ++++++------------ services/platform/Dockerfile.dockerignore | 2 +- .../components/agent-node-fields.tsx | 2 +- .../components/blank-automation-dialog.tsx | 4 +- .../automations/components/trigger-editor.tsx | 2 +- .../chat/components/chat-surface.test.tsx | 4 +- .../features/chat/components/chat-surface.tsx | 4 +- .../chat/components/composer-attachments.tsx | 2 +- .../app/features/chat/components/composer.tsx | 2 +- .../chat/hooks/use-file-indexing-status.ts | 4 +- .../hooks/use-file-transcription-status.ts | 2 +- .../chat/utils/voice-error-messages.ts | 2 +- .../components/contact-info-dialog.tsx | 2 +- .../app/features/contacts/lib/contact-data.ts | 2 +- .../components/conversation-header.tsx | 2 +- .../components/conversations-navigation.tsx | 2 +- .../components/conversations.test.tsx | 2 +- .../components/conversations.tsx | 2 +- .../hooks/use-bulk-actions.test.ts | 2 +- .../conversations/hooks/use-bulk-actions.ts | 2 +- .../hooks/use-conversation-selection.test.ts | 2 +- .../hooks/use-conversation-selection.ts | 2 +- .../conversations/lib/email-connectors.ts | 2 +- .../components/document-history-dialog.tsx | 2 +- .../documents/components/rag-status-badge.tsx | 2 +- .../components/knowledge-entry-add-dialog.tsx | 2 +- .../knowledge-entry-edit-dialog.tsx | 2 +- .../components/product-create-dialog.tsx | 4 +- .../components/product-edit-dialog.tsx | 2 +- .../components/products-import-dialog.tsx | 4 +- .../components/project-agent-dialog.tsx | 2 +- .../settings/audit-logs/hooks/queries.ts | 2 +- .../governance/components/trash-page.tsx | 2 +- .../components/voice-output-policy-editor.tsx | 2 +- .../file-request-dialog.tsx | 2 +- .../data-subject-requests/hooks/queries.ts | 2 +- .../requests-list-section.tsx | 2 +- .../sla-countdown-badge.tsx | 2 +- .../data-subject-requests/status-badge.tsx | 2 +- .../settings/governance/hooks/queries.ts | 4 +- .../app/features/shared/files/types.ts | 2 +- ...ap.test.ts => use-file-upload.cap.test.ts} | 34 +++---- ...se-file-upload.indexing-toast.cap.test.ts} | 8 +- ...upload.test.ts => use-file-upload.test.ts} | 18 ++-- ...nvex-file-upload.ts => use-file-upload.ts} | 6 +- .../shared/mentions/use-kb-mentions.ts | 6 +- .../components/mention-trigger-chips.tsx | 2 +- .../tasks/components/reviewer-picker.tsx | 2 +- .../tasks/components/task-attachments.tsx | 4 +- .../features/tasks/components/task-modal.tsx | 18 ++-- .../app/features/tasks/lib/display.ts | 6 +- .../tasks/lib/mention-actor-options.ts | 4 +- .../app/features/tasks/lib/mention-handles.ts | 8 +- .../components/website-pages-dialog.tsx | 2 +- .../components/website-view-dialog.tsx | 2 +- .../app/features/websites/lib/scan-paused.ts | 2 +- services/platform/app/lib/loader-preload.ts | 2 +- services/platform/backend/auth/auth.ts | 2 +- .../{convex => backend/core}/README.md | 0 .../core}/accounts/microsoft_account.test.ts | 0 .../core}/accounts/microsoft_account.ts | 0 .../core}/agent_secrets/constants.test.ts | 0 .../core}/agent_secrets/constants.ts | 0 .../core}/agents/file_actions.test.ts | 2 +- .../core}/agents/file_actions.ts | 10 +-- .../core}/agents/file_utils.test.ts | 2 +- .../core}/agents/file_utils.ts | 4 +- .../{convex => backend/core}/agents/views.ts | 2 +- .../core}/approvals/policy.test.ts | 2 +- .../core}/approvals/policy.ts | 2 +- .../core}/approvals/types.ts | 0 .../core}/audit_logs/agent_run_ledger.ts | 2 +- .../core}/audit_logs/emit.ts | 0 .../core}/audit_logs/helpers.test.ts | 0 .../core}/audit_logs/helpers.ts | 2 +- .../core}/audit_logs/types.ts | 0 .../core}/automations/agent_host.ts | 2 +- .../core}/automations/agent_retry.ts | 0 .../automations/ask_answer_carryover.test.ts | 0 .../core}/automations/ask_answer_carryover.ts | 0 .../automations/bound_run_payload.test.ts | 2 +- .../core}/automations/bound_run_payload.ts | 4 +- .../core}/automations/checkpoints.ts | 2 +- .../core}/automations/cron.ts | 0 .../core}/automations/liveness.ts | 0 .../core}/automations/llm_call.test.ts | 0 .../core}/automations/llm_call.ts | 2 +- .../core}/automations/pack_zip.test.ts | 2 +- .../core}/automations/pack_zip.ts | 10 +-- .../core}/automations/stepper.ts | 14 +-- .../core}/automations/store.ts | 11 ++- .../core}/automations/upload_impl.ts | 20 ++--- .../core}/automations/webhook_token.ts | 0 .../automations_builder/chat_wire.test.ts | 2 +- .../core}/automations_builder/chat_wire.ts | 12 +-- .../automations_builder/mcp_http.test.ts | 2 +- .../core}/automations_builder/mcp_http.ts | 4 +- .../core}/automations_builder/model_call.ts | 10 +-- .../core}/automations_builder/run_session.ts | 12 +-- .../trusted_headers/get_user_by_id.ts | 2 +- .../trusted_headers/resolve_team_names.ts | 0 .../core}/branding/file_utils.ts | 4 +- .../core}/changelog/internal_actions.ts | 0 .../core}/chat/assistant_tools.test.ts | 8 +- .../core}/chat/assistant_tools.ts | 22 +++-- .../{convex => backend/core}/chat/composer.ts | 0 .../core}/chat/external_turn_shared.ts | 6 +- .../core}/chat/generate_title.ts | 4 +- .../core}/chat/project_context.test.ts | 2 +- .../core}/chat/project_context.ts | 2 +- .../core}/chat/turn_action.ts | 34 +++---- .../core}/chat/turn_store.ts | 6 +- .../cloud_import/deployment_config.test.ts | 0 .../core}/cloud_import/deployment_config.ts | 0 .../core}/cloud_import/providers.ts | 0 .../core}/cloud_import/token_refresh.ts | 2 +- .../core}/cloud_import/types.ts | 0 .../core}/collab/coalesce.ts | 2 +- .../collab/dismiss_review_notifications.ts | 2 +- .../core}/collab/notify_task_reviews.ts | 0 .../{convex => backend/core}/collab/types.ts | 0 .../auth_injection.test.ts | 0 .../connector_credentials/auth_injection.ts | 0 .../connector_catalog.ts | 2 +- .../imap_from_address.test.ts | 0 .../imap_from_address.ts | 0 .../connector_credentials/masking.test.ts | 0 .../core}/connector_credentials/masking.ts | 0 .../core}/connector_credentials/mutations.ts | 2 +- .../resolve_credential.ts | 2 +- .../core}/connector_credentials/types.ts | 0 .../core}/connectors/hostcall_token.test.ts | 0 .../core}/connectors/hostcall_token.ts | 0 .../core}/conversations/README.md | 0 .../core}/conversations/attachments.test.ts | 2 +- .../core}/conversations/attachments.ts | 4 +- .../build_threading_headers.test.ts | 0 .../conversations/build_threading_headers.ts | 0 .../conversations/connector_slug.test.ts | 0 .../core}/conversations/connector_slug.ts | 0 .../ingest/add_message_to_conversation.ts | 0 .../ingest/attachments_for_metadata.test.ts | 0 .../ingest/attachments_for_metadata.ts | 0 .../ingest/bind_email_attachments.test.ts | 0 .../ingest/bind_email_attachments.ts | 2 +- .../ingest/build_conversation_metadata.ts | 0 .../ingest/build_email_metadata.ts | 0 .../ingest/build_initial_message.ts | 0 .../ingest/check_conversation_exists.ts | 0 .../ingest/check_message_exists.ts | 0 .../core}/conversations/ingest/constants.ts | 0 .../create_conversation_from_email.test.ts | 0 .../ingest/create_conversation_from_email.ts | 2 +- ...reate_conversation_from_sent_email.test.ts | 0 .../create_conversation_from_sent_email.ts | 0 .../find_or_create_contact_from_email.test.ts | 0 .../find_or_create_contact_from_email.ts | 0 .../materialize_email_attachments.test.ts | 0 .../ingest/materialize_email_attachments.ts | 2 +- .../ingest/normalize_email.test.ts | 0 .../conversations/ingest/normalize_email.ts | 0 .../normalize_external_message_id.test.ts | 0 .../ingest/normalize_external_message_id.ts | 0 .../ingest/parse_thread_reference_ids.test.ts | 0 .../ingest/parse_thread_reference_ids.ts | 0 .../query_latest_message_by_delivery_state.ts | 0 ...y_latest_outbound_message_for_sync.test.ts | 0 .../query_latest_outbound_message_for_sync.ts | 0 .../ingest/resolve_connector_account_email.ts | 2 +- .../ingest/resolve_contact_email.ts | 2 +- .../resolve_email_conversation_target.test.ts | 0 .../resolve_email_conversation_target.ts | 0 .../ingest/reuse_stored_attachments.test.ts | 0 .../ingest/reuse_stored_attachments.ts | 2 +- .../core}/conversations/ingest/types.ts | 0 .../conversations/ingest/update_message.ts | 0 .../core}/conversations/reply_from.test.ts | 0 .../core}/conversations/reply_from.ts | 2 +- .../conversations/reply_to_conversation.ts | 2 +- .../core}/conversations/send_input.ts | 2 +- .../send_message_via_connector.ts | 2 +- .../core}/conversations/sync_mailbox.test.ts | 2 +- .../core}/conversations/sync_mailbox.ts | 4 +- .../core}/conversations/types.ts | 0 .../core}/deployment/auth_policy.test.ts | 0 .../core}/deployment/auth_policy.ts | 2 +- .../core}/deployment/editors.test.ts | 0 .../core}/deployment/editors.ts | 0 .../core}/deployment/file_utils.ts | 9 +- .../core}/deployment/secret_io.ts | 2 +- .../deployment/test_datastore_connection.ts | 0 .../core}/documents/access.test.ts | 0 .../core}/documents/access.ts | 2 +- .../documents/attest_document_bytes.test.ts | 0 .../core}/documents/attest_document_bytes.ts | 2 +- .../core/documents/extract_extension.ts | 1 + .../core}/documents/get_user_names_batch.ts | 2 +- .../core}/documents/parse_yaml_map.test.ts | 0 .../core}/documents/parse_yaml_map.ts | 0 .../documents/serialize_yaml_map.test.ts | 0 .../core}/documents/serialize_yaml_map.ts | 0 .../core}/enterprise_sso/claims.test.ts | 0 .../core}/enterprise_sso/claims.ts | 2 +- .../core}/enterprise_sso/config/file_store.ts | 2 +- .../enterprise_sso/entra_id/adapter.test.ts | 0 .../core}/enterprise_sso/entra_id/adapter.ts | 0 .../enterprise_sso/entra_id/constants.test.ts | 0 .../enterprise_sso/entra_id/constants.ts | 0 .../enterprise_sso/entra_id/error_codes.ts | 0 .../entra_id/role_mapping.test.ts | 0 .../enterprise_sso/entra_id/role_mapping.ts | 0 .../core}/enterprise_sso/file_utils.ts | 6 +- .../find_or_create_sso_user.test.ts | 0 .../enterprise_sso/find_or_create_sso_user.ts | 2 +- .../generic_oidc/adapter.test.ts | 0 .../enterprise_sso/generic_oidc/adapter.ts | 2 +- .../login/authorize_handler.test.ts | 0 .../enterprise_sso/login/authorize_handler.ts | 0 .../login/callback_handler.test.ts | 0 .../enterprise_sso/login/callback_handler.ts | 0 .../enterprise_sso/login/discover_handler.ts | 0 .../enterprise_sso/login/finish_login.ts | 0 .../core}/enterprise_sso/login/login_audit.ts | 0 .../login/redirect_with_error.ts | 0 .../core}/enterprise_sso/oauth2/adapter.ts | 2 +- .../core}/enterprise_sso/oidc_discovery.ts | 2 +- .../core}/enterprise_sso/pkce.test.ts | 0 .../core}/enterprise_sso/pkce.ts | 0 .../core}/enterprise_sso/registry.ts | 0 .../core}/enterprise_sso/saml/acs_handler.ts | 0 .../enterprise_sso/saml/attributes.test.ts | 0 .../core}/enterprise_sso/saml/attributes.ts | 0 .../enterprise_sso/saml/login_handler.ts | 0 .../enterprise_sso/saml/metadata_handler.ts | 0 .../saml/parse_metadata.test.ts | 0 .../enterprise_sso/saml/parse_metadata.ts | 2 +- .../enterprise_sso/saml/validate_assertion.ts | 0 .../core}/enterprise_sso/sign_cookie_value.ts | 0 .../core}/enterprise_sso/types.ts | 4 +- .../{convex => backend/core}/events/emit.ts | 0 .../core}/feedback/stats.test.ts | 0 .../core}/feedback/stats.ts | 0 .../core}/file_metadata/audio_preprocess.ts | 0 .../core}/file_metadata/paragraphize.ts | 0 .../source_from_provider.test.ts | 0 .../file_metadata/source_from_provider.ts | 0 .../core}/file_metadata/transcribe_audio.ts | 4 +- .../file_metadata/transcribe_dictation.ts | 0 .../transcription_request.test.ts | 0 .../file_metadata/transcription_request.ts | 0 .../google_drive/derive_sync_targets.test.ts | 0 .../core}/google_drive/derive_sync_targets.ts | 0 .../core}/google_drive/get_file_metadata.ts | 2 +- .../core}/google_drive/import_files.ts | 2 +- .../core}/google_drive/list_files.test.ts | 0 .../core}/google_drive/list_files.ts | 2 +- .../google_drive/list_folder_contents.ts | 2 +- .../governance/budget_enforcement.test.ts | 2 +- .../core}/governance/budget_enforcement.ts | 2 +- .../core}/governance/competence.ts | 0 .../core}/governance/cost_estimation.ts | 0 .../core}/governance/dsar_policy.test.ts | 0 .../core}/governance/dsar_policy.ts | 2 +- .../core}/governance/erasure_constants.ts | 0 .../governance/feature_enforcement.test.ts | 0 .../core}/governance/feature_enforcement.ts | 2 +- .../core}/governance/file_utils.ts | 8 +- .../core}/governance/get_org_usage_metrics.ts | 2 +- .../core}/governance/helpers.test.ts | 0 .../core}/governance/helpers.ts | 4 +- .../model_access_enforcement.test.ts | 2 +- .../governance/model_access_enforcement.ts | 4 +- .../governance/resolve_default_model.test.ts | 0 .../core}/governance/resolve_default_model.ts | 2 +- .../retention_bounds_proposal.test.ts | 0 .../governance/retention_bounds_proposal.ts | 4 +- .../core}/governance/retention_floors.test.ts | 2 +- .../core}/governance/retention_floors.ts | 2 +- .../core}/governance/review_policy.ts | 2 +- .../core}/governance/schema.ts | 0 .../governance/session_idle_enforcement.ts | 4 +- .../core}/governance/soft_delete.ts | 0 .../core}/http_connectors/authorize_url.ts | 0 .../http_connectors/deployment_config.ts | 0 .../core}/http_connectors/error_page.ts | 0 .../core}/http_connectors/oauth_state.ts | 0 .../http_connectors/slack_signature.test.ts | 0 .../core}/http_connectors/slack_signature.ts | 0 .../http_connectors/token_exchange.test.ts | 0 .../core}/http_connectors/token_exchange.ts | 2 +- .../core}/identities/external_identities.ts | 0 .../identities/external_identities_helpers.ts | 0 .../core}/knowledge/connection.test.ts | 2 +- .../core}/knowledge/connection.ts | 4 +- .../core}/knowledge/corpus.test.ts | 0 .../core}/knowledge/corpus.ts | 6 +- .../core}/knowledge/crawl.ts | 2 +- .../core}/knowledge/crawl_action.ts | 16 ++-- .../core}/knowledge/ddl.test.ts | 0 .../{convex => backend/core}/knowledge/ddl.ts | 4 +- .../core}/knowledge/dimensions.test.ts | 0 .../core}/knowledge/dimensions.ts | 4 +- .../core}/knowledge/embedding.ts | 8 +- .../core}/knowledge/fetch.test.ts | 2 +- .../core}/knowledge/fetch.ts | 2 +- .../core}/knowledge/indexing.test.ts | 2 +- .../core}/knowledge/indexing.ts | 14 +-- .../core}/knowledge/pii_gate.test.ts | 4 +- .../core}/knowledge/pii_gate.ts | 7 +- .../core}/knowledge/pool.test.ts | 0 .../core}/knowledge/pool.ts | 6 +- .../core}/knowledge/rag_error_codes.ts | 0 .../core}/knowledge/search.test.ts | 2 +- .../core}/knowledge/search.ts | 10 +-- .../core}/knowledge_entries/constants.ts | 0 .../core}/knowledge_entries/helpers.test.ts | 2 +- .../core}/knowledge_entries/helpers.ts | 2 +- .../core}/legacy/knowledge_delete.ts | 4 +- .../core}/lib/age_keygen.ts | 0 .../lib/auth/find_user_by_normalized_email.ts | 0 .../lib/auth/normalize_auth_email.test.ts | 0 .../core}/lib/auth/normalize_auth_email.ts | 0 .../require_org_admin_or_developer.test.ts | 0 .../auth/require_org_admin_or_developer.ts | 4 +- .../lib/auth/require_org_membership.test.ts | 2 +- .../core}/lib/auth/require_org_membership.ts | 2 +- .../core}/lib/config_cache/read.ts | 0 .../core}/lib/config_store/builtin_catalog.ts | 0 .../lib/config_store/read_domain_file.test.ts | 0 .../lib/config_store/read_domain_file.ts | 2 +- .../core}/lib/config_store/resolvers.ts | 0 .../core}/lib/crypto/base64_to_bytes.ts | 0 .../core}/lib/crypto/base64_url_to_buffer.ts | 0 .../core}/lib/crypto/decrypt_string.ts | 0 .../crypto/disarm_broken_to_base64_shim.ts | 0 .../core}/lib/crypto/encrypt_string.ts | 0 .../core}/lib/crypto/get_secret_key.ts | 0 .../core}/lib/crypto/hex_to_bytes.ts | 0 .../{convex => backend/core}/lib/ctx.ts | 2 +- .../{convex => backend/core}/lib/debug_log.ts | 0 .../core}/lib/e2e_cron_guard.ts | 0 .../classify_transcription_error.test.ts | 2 +- .../errors/classify_transcription_error.ts | 0 .../core}/lib/file_io.test.ts | 0 .../{convex => backend/core}/lib/file_io.ts | 4 +- .../core}/lib/get_user_teams.ts | 2 +- .../core}/lib/handler_names.ts | 4 +- .../core}/lib/helpers/audit_hash.test.ts | 0 .../core}/lib/helpers/audit_hash.ts | 2 +- .../core}/lib/helpers/build_audit_context.ts | 0 .../lib/helpers/count_items_in_org.test.ts | 0 .../core}/lib/helpers/count_items_in_org.ts | 0 .../core}/lib/helpers/id_shape.ts | 0 .../core}/lib/helpers/org_slug.test.ts | 0 .../core}/lib/helpers/org_slug.ts | 4 +- .../core}/lib/helpers/pii_hash.test.ts | 0 .../core}/lib/helpers/pii_hash.ts | 0 .../lib/helpers/public_storage_url.test.ts | 0 .../core}/lib/helpers/public_storage_url.ts | 0 .../core}/lib/json/json_path.ts | 2 +- .../lib/knowledge/extraction/docx.test.ts | 0 .../core}/lib/knowledge/extraction/docx.ts | 0 .../core}/lib/knowledge/extraction/helpers.ts | 0 .../core}/lib/knowledge/extraction/image.ts | 0 .../core}/lib/knowledge/extraction/odt.ts | 0 .../core}/lib/knowledge/extraction/ooxml.ts | 0 .../lib/knowledge/extraction/pdf.test.ts | 0 .../core}/lib/knowledge/extraction/pdf.ts | 0 .../extraction/pdfjs_dom_polyfill.test.ts | 0 .../extraction/pdfjs_dom_polyfill.ts | 0 .../lib/knowledge/extraction/pdfjs_loader.ts | 0 .../lib/knowledge/extraction/pptx.test.ts | 0 .../core}/lib/knowledge/extraction/pptx.ts | 0 .../lib/knowledge/extraction/router.test.ts | 0 .../core}/lib/knowledge/extraction/router.ts | 0 .../core}/lib/knowledge/extraction/text.ts | 0 .../lib/knowledge/extraction/vision_client.ts | 0 .../lib/knowledge/extraction/xlsx.test.ts | 0 .../core}/lib/knowledge/extraction/xlsx.ts | 0 .../core}/lib/providers/agent_serving.test.ts | 2 +- .../core}/lib/providers/agent_serving.ts | 8 +- .../core}/lib/providers/catalog_fetch.test.ts | 9 +- .../core}/lib/providers/catalog_fetch.ts | 10 +-- .../core}/lib/providers/chat_catalog.ts | 2 +- .../core}/lib/providers/credential_auth.ts | 4 +- .../core}/lib/providers/direct_credential.ts | 0 .../lib/providers/harness_status.test.ts | 0 .../core}/lib/providers/harness_status.ts | 4 +- .../lib/providers/load_system_config.test.ts | 2 +- .../core}/lib/providers/load_system_config.ts | 6 +- .../core}/lib/providers/org_providers.test.ts | 0 .../core}/lib/providers/org_providers.ts | 6 +- .../lib/providers/resolve_chat_model.test.ts | 2 +- .../core}/lib/providers/resolve_chat_model.ts | 4 +- .../providers/resolve_transcription_model.ts | 2 +- .../lib/providers/resolve_tts_model.test.ts | 2 +- .../core}/lib/providers/resolve_tts_model.ts | 4 +- .../providers/resolve_vision_model.test.ts | 2 +- .../lib/providers/resolve_vision_model.ts | 6 +- .../core}/lib/rest/helpers.test.ts | 0 .../core}/lib/rest/helpers.ts | 4 +- .../core}/lib/rls/MEMBERSHIP_MIRROR_DESIGN.md | 0 .../rls/auth/get_auth_user_identity.test.ts | 0 .../lib/rls/auth/get_auth_user_identity.ts | 0 .../lib/rls/auth/get_authenticated_user.ts | 0 .../lib/rls/auth/get_trusted_auth_data.ts | 2 +- .../rls/auth/require_authenticated_user.ts | 0 .../core}/lib/rls/errors.ts | 2 +- .../lib/rls/helpers/access_control.test.ts | 0 .../core}/lib/rls/helpers/access_control.ts | 0 .../lib/rls/helpers/agent_read_access.ts | 0 .../helpers/conversation_assignment.test.ts | 0 .../rls/helpers/conversation_assignment.ts | 0 .../lib/rls/helpers/role_helpers.test.ts | 0 .../core}/lib/rls/helpers/role_helpers.ts | 0 .../get_organization_member.test.ts | 0 .../organization/get_organization_member.ts | 0 .../get_user_organizations.test.ts | 0 .../organization/get_user_organizations.ts | 2 +- .../{convex => backend/core}/lib/rls/types.ts | 0 .../{convex => backend/core}/lib/rows.ts | 0 .../core}/lib/safe_path_segment.ts | 0 .../core}/lib/search/index.ts | 0 .../core}/lib/search/listing_intent.test.ts | 0 .../core}/lib/search/listing_intent.ts | 5 +- .../core}/lib/search/relevance.test.ts | 0 .../core}/lib/search/relevance.ts | 0 .../core}/lib/search/run_entity_search.ts | 0 .../lib/search/scoped_substring_search.ts | 0 .../core}/lib/search/strategies/contacts.ts | 0 .../core}/lib/search/strategies/documents.ts | 0 .../core}/lib/search/strategies/projects.ts | 0 .../core}/lib/search/strategies/tasks.ts | 0 .../core}/lib/search/types.ts | 0 .../core}/lib/secret_box.ts | 0 .../{convex => backend/core}/lib/sops.ts | 0 .../core}/lib/storage/blob_access.ts | 0 .../core}/lib/storage/blob_delete.ts | 0 .../core}/lib/storage/blob_ref.test.ts | 0 .../core}/lib/storage/blob_ref.ts | 0 .../core}/lib/storage/browser_facing.test.ts | 0 .../core}/lib/storage/object_store.ts | 0 .../core}/lib/storage/sandbox_stage_token.ts | 0 .../core}/lib/team_access.ts | 0 .../core}/lib/types/pdfjs_worker.d.ts | 0 .../core}/lib/utils/client_ip.test.ts | 0 .../core}/lib/utils/client_ip.ts | 0 .../core}/lib/utils/sanitize_secrets.test.ts | 0 .../core}/lib/utils/sanitize_secrets.ts | 0 .../core}/login_attempts/helpers.test.ts | 2 +- .../core}/login_attempts/helpers.ts | 2 +- .../core}/members/mirror_sync.ts | 0 .../{convex => backend/core}/members/types.ts | 0 .../sandbox/connectors_bridge.test.ts | 2 +- .../node_only/sandbox/connectors_bridge.ts | 4 +- .../sandbox/engine_exec_runner.test.ts | 0 .../node_only/sandbox/engine_exec_runner.ts | 4 +- .../sandbox/gateway_provisioning.test.ts | 0 .../node_only/sandbox/gateway_provisioning.ts | 2 +- .../sandbox/helpers/session_client.test.ts | 0 .../sandbox/helpers/session_client.ts | 0 .../node_only/sandbox/helpers/stage_url.ts | 0 .../sandbox/llm_gateway_admin.test.ts | 0 .../node_only/sandbox/llm_gateway_admin.ts | 2 +- .../node_only/sandbox/render_fetch.test.ts | 0 .../core}/node_only/sandbox/render_fetch.ts | 0 .../sandbox/session_credentials.test.ts | 0 .../node_only/sandbox/session_credentials.ts | 0 .../node_only/sandbox/session_exec.test.ts | 2 +- .../core}/node_only/sandbox/session_exec.ts | 0 .../core}/node_only/sandbox/turn_equipment.ts | 0 .../sandbox/workspace_domain_tools.ts | 6 +- .../sandbox/workspace_tool_shared.ts | 0 .../sandbox/workspace_tools_bridge.test.ts | 2 +- .../sandbox/workspace_tools_bridge.ts | 8 +- .../actionable_email_connectors.ts | 0 .../notifications/actionable_email_input.ts | 0 .../core}/notifications/actor_name.ts | 0 .../core}/notifications/helpers.ts | 0 .../notification_messages.test.ts | 6 +- .../notifications/notification_messages.ts | 2 +- .../personal_notification_url.ts | 0 .../core}/notifications/types.ts | 0 .../core}/object_storage/file_utils.ts | 4 +- .../onedrive/derive_sync_targets.test.ts | 0 .../core}/onedrive/derive_sync_targets.ts | 0 .../core}/onedrive/get_file_metadata.ts | 2 +- .../core}/onedrive/import_files.test.ts | 0 .../core}/onedrive/import_files.ts | 2 +- .../core}/onedrive/list_files.ts | 2 +- .../onedrive/list_folder_contents.test.ts | 0 .../core}/onedrive/list_folder_contents.ts | 2 +- .../core}/onedrive/list_sharepoint_drives.ts | 2 +- .../core}/onedrive/list_sharepoint_files.ts | 2 +- .../core}/onedrive/list_sharepoint_sites.ts | 2 +- .../onedrive/reconcile_folder_sync.test.ts | 0 .../core}/onedrive/reconcile_folder_sync.ts | 0 .../core}/onedrive/refresh_token.ts | 2 +- .../organizations/resolve_org_slug.test.ts | 0 .../core}/organizations/resolve_org_slug.ts | 0 .../core}/organizations/scaffold.ts | 6 +- .../core}/products/field_limits.test.ts | 2 +- .../core}/products/field_limits.ts | 2 +- .../core}/projects/access.test.ts | 0 .../core}/projects/access.ts | 0 .../core}/projects/audit_actions.ts | 0 .../core}/projects/resolve_project_access.ts | 2 +- .../provider_credentials/broker_pool.test.ts | 2 +- .../core}/provider_credentials/broker_pool.ts | 6 +- .../provider_credentials/masking.test.ts | 0 .../core}/provider_credentials/masking.ts | 0 .../resolve_credential.ts | 6 +- .../provider_credentials/token_hash.test.ts | 0 .../core}/provider_credentials/token_hash.ts | 0 .../provision_default_automations.test.ts | 2 +- .../provision_default_automations.ts | 4 +- .../core}/sandbox/agent_deadline.test.ts | 0 .../core}/sandbox/agent_deadline.ts | 0 .../core}/sandbox/quota_policy.ts | 4 +- .../core}/sandbox/session_constants.ts | 0 .../core}/sandbox/session_naming.test.ts | 0 .../core}/sandbox/session_naming.ts | 0 .../core}/sandbox/tool_names.test.ts | 0 .../core}/sandbox/tool_names.ts | 0 .../core}/sandbox/user_env_constants.test.ts | 0 .../core}/sandbox/user_env_constants.ts | 0 .../core}/sandbox/workspace_access.ts | 0 .../{convex => backend/core}/scim/data.ts | 0 .../core}/scim/discovery.ts | 0 .../core}/scim/helpers/crypto.ts | 0 .../core}/scim/http_actions.ts | 0 .../core}/scim/internal_mutations.ts | 0 .../{convex => backend/core}/scim/links.ts | 0 .../core}/scim/mappers.test.ts | 0 .../{convex => backend/core}/scim/mappers.ts | 2 +- .../core}/scim/responses.ts | 0 .../{convex => backend/core}/scim/types.ts | 0 .../core}/skills/bundle_zip.test.ts | 4 +- .../core}/skills/bundle_zip.ts | 6 +- .../core}/skills/file_actions.test.ts | 2 +- .../core}/skills/file_actions.ts | 10 +-- .../core}/skills/file_utils.test.ts | 4 +- .../core}/skills/file_utils.ts | 4 +- .../{convex => backend/core}/skills/views.ts | 2 +- .../core}/tasks/access.test.ts | 2 +- .../{convex => backend/core}/tasks/access.ts | 2 +- .../core}/tasks/agent_run_host.ts | 6 +- .../core}/tasks/audit_actions.ts | 0 .../date_notification_recipients.test.ts | 0 .../tasks/date_notification_recipients.ts | 0 .../core}/tasks/helpers.test.ts | 0 .../{convex => backend/core}/tasks/helpers.ts | 4 +- .../core}/tasks/issue_ref.test.ts | 0 .../core}/tasks/issue_ref.ts | 0 .../core}/tasks/mentions.test.ts | 0 .../core}/tasks/mentions.ts | 0 .../core}/tasks/rank.test.ts | 0 .../{convex => backend/core}/tasks/rank.ts | 0 .../core}/tasks/review_shared.ts | 4 +- .../core}/tasks/task_auto_retry.ts | 0 .../core}/tasks/task_kick_resume.test.ts | 0 .../core}/tasks/task_kick_resume.ts | 0 .../core}/tasks/task_serving.test.ts | 2 +- .../core}/tasks/task_serving.ts | 0 .../{convex => backend/core}/tasks/types.ts | 0 .../authenticate_handler.ts | 0 .../{convex => backend/core}/tsconfig.json | 0 .../core}/tts/audio_mime.ts | 0 .../core}/tts/error_codes.ts | 4 +- .../core}/video_links/captions_parser.test.ts | 0 .../core}/video_links/captions_parser.ts | 0 .../core}/video_links/ingest_video_link.ts | 4 +- .../core}/video_links/internal_mutations.ts | 0 .../core}/video_links/url_safety.test.ts | 0 .../core}/video_links/url_safety.ts | 4 +- .../core}/video_links/ytdlp.test.ts | 0 .../core}/video_links/ytdlp.ts | 0 .../core}/video_links/ytdlp_toolchain.ts | 0 .../platform/backend/core/webdav/README.md | 26 ++++++ .../{convex => backend/core}/webdav/SMOKE.md | 0 .../core}/webdav/helpers.ts | 0 .../core}/websites/create_website.ts | 2 +- .../core}/websites/internal_actions.ts | 0 .../core}/websites/match_website_search.ts | 0 .../core}/websites/scan_scheduling.test.ts | 0 .../core}/websites/scan_scheduling.ts | 0 .../core}/websites/types.ts | 0 .../backend/domains/agent_secrets/service.ts | 4 +- .../platform/backend/domains/agents/routes.ts | 12 +-- .../backend/domains/approvals/gate.ts | 2 +- .../backend/domains/audit_logs/routes.ts | 10 +-- .../backend/domains/audit_logs/service.ts | 2 +- .../backend/domains/audit_logs/verify.ts | 2 +- .../domains/automations/dispatch-store.ts | 8 +- .../backend/domains/automations/reattach.ts | 4 +- .../backend/domains/automations/routes.ts | 8 +- .../backend/domains/automations/shim.ts | 2 +- .../backend/domains/automations/store.ts | 2 +- .../backend/domains/automations/triggers.ts | 4 +- .../backend/domains/automations/upload.ts | 8 +- .../backend/domains/branding/service.ts | 6 +- .../domains/browser_sessions/service.ts | 4 +- .../backend/domains/changelog/service.ts | 2 +- .../platform/backend/domains/chat/composer.ts | 12 +-- .../platform/backend/domains/chat/routes.ts | 2 +- .../platform/backend/domains/chat/service.ts | 6 +- .../platform/backend/domains/chat/shim.ts | 4 +- .../platform/backend/domains/chat/threads.ts | 2 +- .../backend/domains/cloud_import/routes.ts | 28 +++--- .../backend/domains/cloud_import/service.ts | 10 +-- .../backend/domains/collab/email-sink.ts | 14 +-- .../domains/collab/mention-directory.ts | 4 +- .../backend/domains/collab/service.ts | 4 +- .../domains/connector_credentials/routes.ts | 4 +- .../domains/connector_credentials/service.ts | 14 +-- .../domains/connectors/bridge-routes.ts | 10 +-- .../domains/connectors/oauth-app-routes.ts | 4 +- .../domains/connectors/oauth-apps.test.ts | 2 +- .../backend/domains/connectors/oauth-apps.ts | 10 +-- .../domains/connectors/oauth-routes.ts | 4 +- .../backend/domains/connectors/oauth.ts | 12 +-- .../backend/domains/connectors/service.ts | 20 ++--- .../domains/connectors/slack-events.ts | 10 +-- .../backend/domains/connectors/sso-reuse.ts | 2 +- .../backend/domains/control/service.ts | 2 +- .../backend/domains/conversations/routing.ts | 2 +- .../domains/conversations/search-chat.ts | 6 +- .../backend/domains/conversations/send.ts | 16 ++-- .../backend/domains/conversations/service.ts | 2 +- .../backend/domains/deployment/service.ts | 45 +++++----- .../backend/domains/documents/project-text.ts | 8 +- .../backend/domains/documents/records.ts | 2 +- .../backend/domains/documents/replacement.ts | 18 ++-- .../backend/domains/documents/service.ts | 4 +- .../backend/domains/feedback/service.ts | 3 +- .../domains/file_metadata/watchdogs.ts | 2 +- .../domains/files/sandbox-blob-routes.ts | 4 +- .../platform/backend/domains/files/service.ts | 9 +- .../backend/domains/files/transcription.ts | 14 +-- .../backend/domains/folders/service.ts | 4 +- .../backend/domains/google_drive/routes.ts | 4 +- .../backend/domains/google_drive/service.ts | 6 +- .../backend/domains/governance/routes.ts | 16 ++-- .../backend/domains/governance/service.ts | 16 ++-- .../domains/governance/session-idle.ts | 2 +- .../domains/governance/settings-tail.ts | 7 +- .../domains/governance/usage-metrics.ts | 2 +- .../backend/domains/identities/service.ts | 2 +- .../backend/domains/knowledge/admin.ts | 40 ++++----- .../backend/domains/knowledge/service.ts | 28 +++--- .../domains/knowledge_entries/service.ts | 2 +- .../backend/domains/login_attempts/service.ts | 8 +- .../domains/object_storage/bootstrap.ts | 16 ++-- .../backend/domains/object_storage/service.ts | 16 ++-- .../backend/domains/onedrive/routes.ts | 10 +-- .../backend/domains/onedrive/service.ts | 32 +++---- .../backend/domains/organizations/scaffold.ts | 2 +- .../backend/domains/projects/secrets.ts | 2 +- .../backend/domains/projects/service.ts | 22 ++--- .../domains/provider_credentials/service.ts | 14 +-- .../backend/domains/providers/routes.ts | 22 ++--- .../backend/domains/provisioning/service.ts | 2 +- .../backend/domains/retention/routes.ts | 22 ++--- .../backend/domains/retention/service.ts | 20 ++--- .../domains/sandbox/dispatch-routes.ts | 4 +- .../backend/domains/sandbox/recovery.ts | 2 +- .../backend/domains/sandbox/routes.ts | 14 +-- .../backend/domains/sandbox/service.ts | 2 +- .../backend/domains/sandbox/sessions.ts | 8 +- .../platform/backend/domains/sandbox/shim.ts | 2 +- .../backend/domains/sandbox/user-env.ts | 4 +- .../platform/backend/domains/scim/routes.ts | 22 ++--- .../platform/backend/domains/scim/service.ts | 6 +- .../platform/backend/domains/scim/shim.ts | 2 +- .../platform/backend/domains/skills/routes.ts | 14 +-- .../platform/backend/domains/skills/upload.ts | 26 +++--- .../platform/backend/domains/sso/admin.ts | 18 ++-- .../platform/backend/domains/sso/config.ts | 6 +- .../platform/backend/domains/sso/routes.ts | 18 ++-- .../platform/backend/domains/sso/service.ts | 10 +-- services/platform/backend/domains/sso/shim.ts | 4 +- .../backend/domains/sso/trusted-headers.ts | 6 +- .../backend/domains/tasks/agent-runs.ts | 2 +- .../backend/domains/tasks/agent-turn-shim.ts | 6 +- .../backend/domains/tasks/comments.ts | 4 +- .../domains/tasks/date-notifications.ts | 2 +- .../backend/domains/tasks/external-ref.ts | 4 +- .../backend/domains/tasks/kick-plan.ts | 2 +- .../backend/domains/tasks/reattach.ts | 2 +- .../platform/backend/domains/tasks/reviews.ts | 2 +- .../platform/backend/domains/tasks/routes.ts | 4 +- .../platform/backend/domains/tasks/service.ts | 18 ++-- .../platform/backend/domains/teams/routes.ts | 2 +- .../platform/backend/domains/tts/routes.ts | 6 +- .../platform/backend/domains/tts/service.ts | 42 ++++----- .../backend/domains/two_factor/service.ts | 6 +- .../backend/domains/video_links/service.ts | 4 +- .../backend/domains/webdav/connector-store.ts | 2 +- .../backend/domains/webdav/handlers.ts | 14 +-- .../platform/backend/domains/webdav/routes.ts | 16 ++-- .../backend/domains/websites/service.ts | 34 +++---- .../platform/backend/integration-check.ts | 43 +++++---- services/platform/backend/jobs/task-list.ts | 14 +-- .../lib/{convex-shim.ts => ctx-shim.ts} | 6 +- .../backend/lib/governance-policies.ts | 2 +- .../backend/lib/governance-policy-write.ts | 6 +- services/platform/backend/lib/object-store.ts | 4 +- services/platform/backend/lib/org-config.ts | Bin 4803 -> 4793 bytes services/platform/backend/node-loader.mjs | 14 ++- .../backend/realtime/oracle-routes.ts | 2 +- services/platform/backend/rest/shared.ts | 2 +- services/platform/backend/rest/v1-core.ts | 12 +-- services/platform/backend/rest/v1-mcp.ts | 12 +-- services/platform/backend/rest/v1-websites.ts | 2 +- .../convex/documents/extract_extension.ts | 1 - services/platform/convex/webdav/README.md | 25 ------ services/platform/docker-entrypoint.sh | 21 +++-- services/platform/lib/chat/context.test.ts | 2 +- services/platform/lib/chat/context.ts | 2 +- .../chat/untrusted-content.test.ts} | 2 +- .../chat/untrusted-content.ts} | 2 +- services/platform/lib/connectors/live-host.ts | 8 +- .../lib/harnesses/exec-builder.test.ts | 2 +- .../lib/harnesses/golden-exec.test.ts | 2 +- .../platform/lib/harnesses/registry.test.ts | 2 +- .../host_policy.ts => lib/net/host-policy.ts} | 4 +- .../net/safe-fetch.test.ts} | 2 +- .../safe_fetch.ts => lib/net/safe-fetch.ts} | 2 +- services/platform/lib/permissions/ability.ts | 4 +- services/platform/lib/shared/chat-errors.ts | 2 +- .../{convex-enums.ts => product-enums.ts} | 2 +- services/platform/lib/shared/file-types.ts | 6 +- .../lib/shared/handlers/function-refs.ts | 2 +- ...ex-error.test.ts => backend-error.test.ts} | 0 .../platform/lib/webdav/auth-parity.test.ts | 17 ++-- services/platform/lib/webdav/auth.ts | 21 +++-- services/platform/lib/webdav/handler.ts | 4 +- services/platform/lib/webdav/locks.ts | 6 +- .../platform/lib/webdav/methods/delete.ts | 8 +- services/platform/lib/webdav/methods/get.ts | 8 +- services/platform/lib/webdav/methods/lock.ts | 8 +- services/platform/lib/webdav/methods/mkcol.ts | 2 +- services/platform/lib/webdav/methods/move.ts | 6 +- .../platform/lib/webdav/methods/propfind.ts | 8 +- .../platform/lib/webdav/methods/proppatch.ts | 2 +- services/platform/lib/webdav/methods/put.ts | 14 +-- .../platform/lib/webdav/methods/unlock.ts | 2 +- services/platform/lib/webdav/test-helpers.ts | 12 +-- services/platform/lib/webdav/types.ts | 16 ++-- services/platform/scripts/dev-engine.ts | 2 +- .../scripts/validate-builtin-configs.ts | 2 +- services/platform/vitest.ui.config.ts | 2 +- services/sandbox/src/wire.ts | 32 +++---- tools/cli/scripts/generate-embedded.ts | 2 +- 762 files changed, 1254 insertions(+), 1323 deletions(-) rename services/platform/app/features/shared/files/{use-convex-file-upload.cap.test.ts => use-file-upload.cap.test.ts} (93%) rename services/platform/app/features/shared/files/{use-convex-file-upload.indexing-toast.cap.test.ts => use-file-upload.indexing-toast.cap.test.ts} (93%) rename services/platform/app/features/shared/files/{use-convex-file-upload.test.ts => use-file-upload.test.ts} (96%) rename services/platform/app/features/shared/files/{use-convex-file-upload.ts => use-file-upload.ts} (99%) rename services/platform/{convex => backend/core}/README.md (100%) rename services/platform/{convex => backend/core}/accounts/microsoft_account.test.ts (100%) rename services/platform/{convex => backend/core}/accounts/microsoft_account.ts (100%) rename services/platform/{convex => backend/core}/agent_secrets/constants.test.ts (100%) rename services/platform/{convex => backend/core}/agent_secrets/constants.ts (100%) rename services/platform/{convex => backend/core}/agents/file_actions.test.ts (99%) rename services/platform/{convex => backend/core}/agents/file_actions.ts (98%) rename services/platform/{convex => backend/core}/agents/file_utils.test.ts (98%) rename services/platform/{convex => backend/core}/agents/file_utils.ts (98%) rename services/platform/{convex => backend/core}/agents/views.ts (98%) rename services/platform/{convex => backend/core}/approvals/policy.test.ts (97%) rename services/platform/{convex => backend/core}/approvals/policy.ts (97%) rename services/platform/{convex => backend/core}/approvals/types.ts (100%) rename services/platform/{convex => backend/core}/audit_logs/agent_run_ledger.ts (99%) rename services/platform/{convex => backend/core}/audit_logs/emit.ts (100%) rename services/platform/{convex => backend/core}/audit_logs/helpers.test.ts (100%) rename services/platform/{convex => backend/core}/audit_logs/helpers.ts (99%) rename services/platform/{convex => backend/core}/audit_logs/types.ts (100%) rename services/platform/{convex => backend/core}/automations/agent_host.ts (99%) rename services/platform/{convex => backend/core}/automations/agent_retry.ts (100%) rename services/platform/{convex => backend/core}/automations/ask_answer_carryover.test.ts (100%) rename services/platform/{convex => backend/core}/automations/ask_answer_carryover.ts (100%) rename services/platform/{convex => backend/core}/automations/bound_run_payload.test.ts (98%) rename services/platform/{convex => backend/core}/automations/bound_run_payload.ts (97%) rename services/platform/{convex => backend/core}/automations/checkpoints.ts (99%) rename services/platform/{convex => backend/core}/automations/cron.ts (100%) rename services/platform/{convex => backend/core}/automations/liveness.ts (100%) rename services/platform/{convex => backend/core}/automations/llm_call.test.ts (100%) rename services/platform/{convex => backend/core}/automations/llm_call.ts (99%) rename services/platform/{convex => backend/core}/automations/pack_zip.test.ts (99%) rename services/platform/{convex => backend/core}/automations/pack_zip.ts (97%) rename services/platform/{convex => backend/core}/automations/stepper.ts (99%) rename services/platform/{convex => backend/core}/automations/store.ts (99%) rename services/platform/{convex => backend/core}/automations/upload_impl.ts (96%) rename services/platform/{convex => backend/core}/automations/webhook_token.ts (100%) rename services/platform/{convex => backend/core}/automations_builder/chat_wire.test.ts (99%) rename services/platform/{convex => backend/core}/automations_builder/chat_wire.ts (97%) rename services/platform/{convex => backend/core}/automations_builder/mcp_http.test.ts (99%) rename services/platform/{convex => backend/core}/automations_builder/mcp_http.ts (98%) rename services/platform/{convex => backend/core}/automations_builder/model_call.ts (95%) rename services/platform/{convex => backend/core}/automations_builder/run_session.ts (90%) rename services/platform/{convex => backend/core}/betterAuth/trusted_headers/get_user_by_id.ts (97%) rename services/platform/{convex => backend/core}/betterAuth/trusted_headers/resolve_team_names.ts (100%) rename services/platform/{convex => backend/core}/branding/file_utils.ts (97%) rename services/platform/{convex => backend/core}/changelog/internal_actions.ts (100%) rename services/platform/{convex => backend/core}/chat/assistant_tools.test.ts (99%) rename services/platform/{convex => backend/core}/chat/assistant_tools.ts (99%) rename services/platform/{convex => backend/core}/chat/composer.ts (100%) rename services/platform/{convex => backend/core}/chat/external_turn_shared.ts (99%) rename services/platform/{convex => backend/core}/chat/generate_title.ts (98%) rename services/platform/{convex => backend/core}/chat/project_context.test.ts (98%) rename services/platform/{convex => backend/core}/chat/project_context.ts (96%) rename services/platform/{convex => backend/core}/chat/turn_action.ts (97%) rename services/platform/{convex => backend/core}/chat/turn_store.ts (97%) rename services/platform/{convex => backend/core}/cloud_import/deployment_config.test.ts (100%) rename services/platform/{convex => backend/core}/cloud_import/deployment_config.ts (100%) rename services/platform/{convex => backend/core}/cloud_import/providers.ts (100%) rename services/platform/{convex => backend/core}/cloud_import/token_refresh.ts (96%) rename services/platform/{convex => backend/core}/cloud_import/types.ts (100%) rename services/platform/{convex => backend/core}/collab/coalesce.ts (99%) rename services/platform/{convex => backend/core}/collab/dismiss_review_notifications.ts (98%) rename services/platform/{convex => backend/core}/collab/notify_task_reviews.ts (100%) rename services/platform/{convex => backend/core}/collab/types.ts (100%) rename services/platform/{convex => backend/core}/connector_credentials/auth_injection.test.ts (100%) rename services/platform/{convex => backend/core}/connector_credentials/auth_injection.ts (100%) rename services/platform/{convex => backend/core}/connector_credentials/connector_catalog.ts (99%) rename services/platform/{convex => backend/core}/connector_credentials/imap_from_address.test.ts (100%) rename services/platform/{convex => backend/core}/connector_credentials/imap_from_address.ts (100%) rename services/platform/{convex => backend/core}/connector_credentials/masking.test.ts (100%) rename services/platform/{convex => backend/core}/connector_credentials/masking.ts (100%) rename services/platform/{convex => backend/core}/connector_credentials/mutations.ts (96%) rename services/platform/{convex => backend/core}/connector_credentials/resolve_credential.ts (99%) rename services/platform/{convex => backend/core}/connector_credentials/types.ts (100%) rename services/platform/{convex => backend/core}/connectors/hostcall_token.test.ts (100%) rename services/platform/{convex => backend/core}/connectors/hostcall_token.ts (100%) rename services/platform/{convex => backend/core}/conversations/README.md (100%) rename services/platform/{convex => backend/core}/conversations/attachments.test.ts (97%) rename services/platform/{convex => backend/core}/conversations/attachments.ts (95%) rename services/platform/{convex => backend/core}/conversations/build_threading_headers.test.ts (100%) rename services/platform/{convex => backend/core}/conversations/build_threading_headers.ts (100%) rename services/platform/{convex => backend/core}/conversations/connector_slug.test.ts (100%) rename services/platform/{convex => backend/core}/conversations/connector_slug.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/add_message_to_conversation.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/attachments_for_metadata.test.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/attachments_for_metadata.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/bind_email_attachments.test.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/bind_email_attachments.ts (98%) rename services/platform/{convex => backend/core}/conversations/ingest/build_conversation_metadata.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/build_email_metadata.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/build_initial_message.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/check_conversation_exists.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/check_message_exists.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/constants.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/create_conversation_from_email.test.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/create_conversation_from_email.ts (99%) rename services/platform/{convex => backend/core}/conversations/ingest/create_conversation_from_sent_email.test.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/create_conversation_from_sent_email.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/find_or_create_contact_from_email.test.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/find_or_create_contact_from_email.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/materialize_email_attachments.test.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/materialize_email_attachments.ts (98%) rename services/platform/{convex => backend/core}/conversations/ingest/normalize_email.test.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/normalize_email.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/normalize_external_message_id.test.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/normalize_external_message_id.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/parse_thread_reference_ids.test.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/parse_thread_reference_ids.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/query_latest_message_by_delivery_state.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/query_latest_outbound_message_for_sync.test.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/query_latest_outbound_message_for_sync.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/resolve_connector_account_email.ts (96%) rename services/platform/{convex => backend/core}/conversations/ingest/resolve_contact_email.ts (91%) rename services/platform/{convex => backend/core}/conversations/ingest/resolve_email_conversation_target.test.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/resolve_email_conversation_target.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/reuse_stored_attachments.test.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/reuse_stored_attachments.ts (98%) rename services/platform/{convex => backend/core}/conversations/ingest/types.ts (100%) rename services/platform/{convex => backend/core}/conversations/ingest/update_message.ts (100%) rename services/platform/{convex => backend/core}/conversations/reply_from.test.ts (100%) rename services/platform/{convex => backend/core}/conversations/reply_from.ts (98%) rename services/platform/{convex => backend/core}/conversations/reply_to_conversation.ts (98%) rename services/platform/{convex => backend/core}/conversations/send_input.ts (98%) rename services/platform/{convex => backend/core}/conversations/send_message_via_connector.ts (99%) rename services/platform/{convex => backend/core}/conversations/sync_mailbox.test.ts (99%) rename services/platform/{convex => backend/core}/conversations/sync_mailbox.ts (99%) rename services/platform/{convex => backend/core}/conversations/types.ts (100%) rename services/platform/{convex => backend/core}/deployment/auth_policy.test.ts (100%) rename services/platform/{convex => backend/core}/deployment/auth_policy.ts (96%) rename services/platform/{convex => backend/core}/deployment/editors.test.ts (100%) rename services/platform/{convex => backend/core}/deployment/editors.ts (100%) rename services/platform/{convex => backend/core}/deployment/file_utils.ts (94%) rename services/platform/{convex => backend/core}/deployment/secret_io.ts (99%) rename services/platform/{convex => backend/core}/deployment/test_datastore_connection.ts (100%) rename services/platform/{convex => backend/core}/documents/access.test.ts (100%) rename services/platform/{convex => backend/core}/documents/access.ts (99%) rename services/platform/{convex => backend/core}/documents/attest_document_bytes.test.ts (100%) rename services/platform/{convex => backend/core}/documents/attest_document_bytes.ts (99%) create mode 100644 services/platform/backend/core/documents/extract_extension.ts rename services/platform/{convex => backend/core}/documents/get_user_names_batch.ts (98%) rename services/platform/{convex => backend/core}/documents/parse_yaml_map.test.ts (100%) rename services/platform/{convex => backend/core}/documents/parse_yaml_map.ts (100%) rename services/platform/{convex => backend/core}/documents/serialize_yaml_map.test.ts (100%) rename services/platform/{convex => backend/core}/documents/serialize_yaml_map.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/claims.test.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/claims.ts (97%) rename services/platform/{convex => backend/core}/enterprise_sso/config/file_store.ts (98%) rename services/platform/{convex => backend/core}/enterprise_sso/entra_id/adapter.test.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/entra_id/adapter.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/entra_id/constants.test.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/entra_id/constants.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/entra_id/error_codes.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/entra_id/role_mapping.test.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/entra_id/role_mapping.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/file_utils.ts (96%) rename services/platform/{convex => backend/core}/enterprise_sso/find_or_create_sso_user.test.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/find_or_create_sso_user.ts (99%) rename services/platform/{convex => backend/core}/enterprise_sso/generic_oidc/adapter.test.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/generic_oidc/adapter.ts (99%) rename services/platform/{convex => backend/core}/enterprise_sso/login/authorize_handler.test.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/login/authorize_handler.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/login/callback_handler.test.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/login/callback_handler.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/login/discover_handler.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/login/finish_login.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/login/login_audit.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/login/redirect_with_error.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/oauth2/adapter.ts (98%) rename services/platform/{convex => backend/core}/enterprise_sso/oidc_discovery.ts (98%) rename services/platform/{convex => backend/core}/enterprise_sso/pkce.test.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/pkce.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/registry.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/saml/acs_handler.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/saml/attributes.test.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/saml/attributes.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/saml/login_handler.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/saml/metadata_handler.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/saml/parse_metadata.test.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/saml/parse_metadata.ts (98%) rename services/platform/{convex => backend/core}/enterprise_sso/saml/validate_assertion.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/sign_cookie_value.ts (100%) rename services/platform/{convex => backend/core}/enterprise_sso/types.ts (95%) rename services/platform/{convex => backend/core}/events/emit.ts (100%) rename services/platform/{convex => backend/core}/feedback/stats.test.ts (100%) rename services/platform/{convex => backend/core}/feedback/stats.ts (100%) rename services/platform/{convex => backend/core}/file_metadata/audio_preprocess.ts (100%) rename services/platform/{convex => backend/core}/file_metadata/paragraphize.ts (100%) rename services/platform/{convex => backend/core}/file_metadata/source_from_provider.test.ts (100%) rename services/platform/{convex => backend/core}/file_metadata/source_from_provider.ts (100%) rename services/platform/{convex => backend/core}/file_metadata/transcribe_audio.ts (99%) rename services/platform/{convex => backend/core}/file_metadata/transcribe_dictation.ts (100%) rename services/platform/{convex => backend/core}/file_metadata/transcription_request.test.ts (100%) rename services/platform/{convex => backend/core}/file_metadata/transcription_request.ts (100%) rename services/platform/{convex => backend/core}/google_drive/derive_sync_targets.test.ts (100%) rename services/platform/{convex => backend/core}/google_drive/derive_sync_targets.ts (100%) rename services/platform/{convex => backend/core}/google_drive/get_file_metadata.ts (96%) rename services/platform/{convex => backend/core}/google_drive/import_files.ts (99%) rename services/platform/{convex => backend/core}/google_drive/list_files.test.ts (100%) rename services/platform/{convex => backend/core}/google_drive/list_files.ts (98%) rename services/platform/{convex => backend/core}/google_drive/list_folder_contents.ts (98%) rename services/platform/{convex => backend/core}/governance/budget_enforcement.test.ts (99%) rename services/platform/{convex => backend/core}/governance/budget_enforcement.ts (99%) rename services/platform/{convex => backend/core}/governance/competence.ts (100%) rename services/platform/{convex => backend/core}/governance/cost_estimation.ts (100%) rename services/platform/{convex => backend/core}/governance/dsar_policy.test.ts (100%) rename services/platform/{convex => backend/core}/governance/dsar_policy.ts (97%) rename services/platform/{convex => backend/core}/governance/erasure_constants.ts (100%) rename services/platform/{convex => backend/core}/governance/feature_enforcement.test.ts (100%) rename services/platform/{convex => backend/core}/governance/feature_enforcement.ts (98%) rename services/platform/{convex => backend/core}/governance/file_utils.ts (97%) rename services/platform/{convex => backend/core}/governance/get_org_usage_metrics.ts (99%) rename services/platform/{convex => backend/core}/governance/helpers.test.ts (100%) rename services/platform/{convex => backend/core}/governance/helpers.ts (98%) rename services/platform/{convex => backend/core}/governance/model_access_enforcement.test.ts (98%) rename services/platform/{convex => backend/core}/governance/model_access_enforcement.ts (97%) rename services/platform/{convex => backend/core}/governance/resolve_default_model.test.ts (100%) rename services/platform/{convex => backend/core}/governance/resolve_default_model.ts (97%) rename services/platform/{convex => backend/core}/governance/retention_bounds_proposal.test.ts (100%) rename services/platform/{convex => backend/core}/governance/retention_bounds_proposal.ts (97%) rename services/platform/{convex => backend/core}/governance/retention_floors.test.ts (99%) rename services/platform/{convex => backend/core}/governance/retention_floors.ts (99%) rename services/platform/{convex => backend/core}/governance/review_policy.ts (97%) rename services/platform/{convex => backend/core}/governance/schema.ts (100%) rename services/platform/{convex => backend/core}/governance/session_idle_enforcement.ts (91%) rename services/platform/{convex => backend/core}/governance/soft_delete.ts (100%) rename services/platform/{convex => backend/core}/http_connectors/authorize_url.ts (100%) rename services/platform/{convex => backend/core}/http_connectors/deployment_config.ts (100%) rename services/platform/{convex => backend/core}/http_connectors/error_page.ts (100%) rename services/platform/{convex => backend/core}/http_connectors/oauth_state.ts (100%) rename services/platform/{convex => backend/core}/http_connectors/slack_signature.test.ts (100%) rename services/platform/{convex => backend/core}/http_connectors/slack_signature.ts (100%) rename services/platform/{convex => backend/core}/http_connectors/token_exchange.test.ts (100%) rename services/platform/{convex => backend/core}/http_connectors/token_exchange.ts (98%) rename services/platform/{convex => backend/core}/identities/external_identities.ts (100%) rename services/platform/{convex => backend/core}/identities/external_identities_helpers.ts (100%) rename services/platform/{convex => backend/core}/knowledge/connection.test.ts (98%) rename services/platform/{convex => backend/core}/knowledge/connection.ts (98%) rename services/platform/{convex => backend/core}/knowledge/corpus.test.ts (100%) rename services/platform/{convex => backend/core}/knowledge/corpus.ts (99%) rename services/platform/{convex => backend/core}/knowledge/crawl.ts (99%) rename services/platform/{convex => backend/core}/knowledge/crawl_action.ts (99%) rename services/platform/{convex => backend/core}/knowledge/ddl.test.ts (100%) rename services/platform/{convex => backend/core}/knowledge/ddl.ts (99%) rename services/platform/{convex => backend/core}/knowledge/dimensions.test.ts (100%) rename services/platform/{convex => backend/core}/knowledge/dimensions.ts (98%) rename services/platform/{convex => backend/core}/knowledge/embedding.ts (97%) rename services/platform/{convex => backend/core}/knowledge/fetch.test.ts (99%) rename services/platform/{convex => backend/core}/knowledge/fetch.ts (99%) rename services/platform/{convex => backend/core}/knowledge/indexing.test.ts (99%) rename services/platform/{convex => backend/core}/knowledge/indexing.ts (97%) rename services/platform/{convex => backend/core}/knowledge/pii_gate.test.ts (98%) rename services/platform/{convex => backend/core}/knowledge/pii_gate.ts (97%) rename services/platform/{convex => backend/core}/knowledge/pool.test.ts (100%) rename services/platform/{convex => backend/core}/knowledge/pool.ts (98%) rename services/platform/{convex => backend/core}/knowledge/rag_error_codes.ts (100%) rename services/platform/{convex => backend/core}/knowledge/search.test.ts (97%) rename services/platform/{convex => backend/core}/knowledge/search.ts (96%) rename services/platform/{convex => backend/core}/knowledge_entries/constants.ts (100%) rename services/platform/{convex => backend/core}/knowledge_entries/helpers.test.ts (99%) rename services/platform/{convex => backend/core}/knowledge_entries/helpers.ts (98%) rename services/platform/{convex => backend/core}/legacy/knowledge_delete.ts (98%) rename services/platform/{convex => backend/core}/lib/age_keygen.ts (100%) rename services/platform/{convex => backend/core}/lib/auth/find_user_by_normalized_email.ts (100%) rename services/platform/{convex => backend/core}/lib/auth/normalize_auth_email.test.ts (100%) rename services/platform/{convex => backend/core}/lib/auth/normalize_auth_email.ts (100%) rename services/platform/{convex => backend/core}/lib/auth/require_org_admin_or_developer.test.ts (100%) rename services/platform/{convex => backend/core}/lib/auth/require_org_admin_or_developer.ts (92%) rename services/platform/{convex => backend/core}/lib/auth/require_org_membership.test.ts (98%) rename services/platform/{convex => backend/core}/lib/auth/require_org_membership.ts (98%) rename services/platform/{convex => backend/core}/lib/config_cache/read.ts (100%) rename services/platform/{convex => backend/core}/lib/config_store/builtin_catalog.ts (100%) rename services/platform/{convex => backend/core}/lib/config_store/read_domain_file.test.ts (100%) rename services/platform/{convex => backend/core}/lib/config_store/read_domain_file.ts (97%) rename services/platform/{convex => backend/core}/lib/config_store/resolvers.ts (100%) rename services/platform/{convex => backend/core}/lib/crypto/base64_to_bytes.ts (100%) rename services/platform/{convex => backend/core}/lib/crypto/base64_url_to_buffer.ts (100%) rename services/platform/{convex => backend/core}/lib/crypto/decrypt_string.ts (100%) rename services/platform/{convex => backend/core}/lib/crypto/disarm_broken_to_base64_shim.ts (100%) rename services/platform/{convex => backend/core}/lib/crypto/encrypt_string.ts (100%) rename services/platform/{convex => backend/core}/lib/crypto/get_secret_key.ts (100%) rename services/platform/{convex => backend/core}/lib/crypto/hex_to_bytes.ts (100%) rename services/platform/{convex => backend/core}/lib/ctx.ts (99%) rename services/platform/{convex => backend/core}/lib/debug_log.ts (100%) rename services/platform/{convex => backend/core}/lib/e2e_cron_guard.ts (100%) rename services/platform/{convex => backend/core}/lib/errors/classify_transcription_error.test.ts (95%) rename services/platform/{convex => backend/core}/lib/errors/classify_transcription_error.ts (100%) rename services/platform/{convex => backend/core}/lib/file_io.test.ts (100%) rename services/platform/{convex => backend/core}/lib/file_io.ts (98%) rename services/platform/{convex => backend/core}/lib/get_user_teams.ts (98%) rename services/platform/{convex => backend/core}/lib/handler_names.ts (99%) rename services/platform/{convex => backend/core}/lib/helpers/audit_hash.test.ts (100%) rename services/platform/{convex => backend/core}/lib/helpers/audit_hash.ts (99%) rename services/platform/{convex => backend/core}/lib/helpers/build_audit_context.ts (100%) rename services/platform/{convex => backend/core}/lib/helpers/count_items_in_org.test.ts (100%) rename services/platform/{convex => backend/core}/lib/helpers/count_items_in_org.ts (100%) rename services/platform/{convex => backend/core}/lib/helpers/id_shape.ts (100%) rename services/platform/{convex => backend/core}/lib/helpers/org_slug.test.ts (100%) rename services/platform/{convex => backend/core}/lib/helpers/org_slug.ts (98%) rename services/platform/{convex => backend/core}/lib/helpers/pii_hash.test.ts (100%) rename services/platform/{convex => backend/core}/lib/helpers/pii_hash.ts (100%) rename services/platform/{convex => backend/core}/lib/helpers/public_storage_url.test.ts (100%) rename services/platform/{convex => backend/core}/lib/helpers/public_storage_url.ts (100%) rename services/platform/{convex => backend/core}/lib/json/json_path.ts (96%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/docx.test.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/docx.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/helpers.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/image.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/odt.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/ooxml.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/pdf.test.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/pdf.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/pdfjs_dom_polyfill.test.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/pdfjs_dom_polyfill.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/pdfjs_loader.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/pptx.test.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/pptx.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/router.test.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/router.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/text.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/vision_client.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/xlsx.test.ts (100%) rename services/platform/{convex => backend/core}/lib/knowledge/extraction/xlsx.ts (100%) rename services/platform/{convex => backend/core}/lib/providers/agent_serving.test.ts (99%) rename services/platform/{convex => backend/core}/lib/providers/agent_serving.ts (98%) rename services/platform/{convex => backend/core}/lib/providers/catalog_fetch.test.ts (97%) rename services/platform/{convex => backend/core}/lib/providers/catalog_fetch.ts (97%) rename services/platform/{convex => backend/core}/lib/providers/chat_catalog.ts (97%) rename services/platform/{convex => backend/core}/lib/providers/credential_auth.ts (91%) rename services/platform/{convex => backend/core}/lib/providers/direct_credential.ts (100%) rename services/platform/{convex => backend/core}/lib/providers/harness_status.test.ts (100%) rename services/platform/{convex => backend/core}/lib/providers/harness_status.ts (97%) rename services/platform/{convex => backend/core}/lib/providers/load_system_config.test.ts (99%) rename services/platform/{convex => backend/core}/lib/providers/load_system_config.ts (98%) rename services/platform/{convex => backend/core}/lib/providers/org_providers.test.ts (100%) rename services/platform/{convex => backend/core}/lib/providers/org_providers.ts (96%) rename services/platform/{convex => backend/core}/lib/providers/resolve_chat_model.test.ts (99%) rename services/platform/{convex => backend/core}/lib/providers/resolve_chat_model.ts (98%) rename services/platform/{convex => backend/core}/lib/providers/resolve_transcription_model.ts (97%) rename services/platform/{convex => backend/core}/lib/providers/resolve_tts_model.test.ts (98%) rename services/platform/{convex => backend/core}/lib/providers/resolve_tts_model.ts (96%) rename services/platform/{convex => backend/core}/lib/providers/resolve_vision_model.test.ts (99%) rename services/platform/{convex => backend/core}/lib/providers/resolve_vision_model.ts (97%) rename services/platform/{convex => backend/core}/lib/rest/helpers.test.ts (100%) rename services/platform/{convex => backend/core}/lib/rest/helpers.ts (99%) rename services/platform/{convex => backend/core}/lib/rls/MEMBERSHIP_MIRROR_DESIGN.md (100%) rename services/platform/{convex => backend/core}/lib/rls/auth/get_auth_user_identity.test.ts (100%) rename services/platform/{convex => backend/core}/lib/rls/auth/get_auth_user_identity.ts (100%) rename services/platform/{convex => backend/core}/lib/rls/auth/get_authenticated_user.ts (100%) rename services/platform/{convex => backend/core}/lib/rls/auth/get_trusted_auth_data.ts (96%) rename services/platform/{convex => backend/core}/lib/rls/auth/require_authenticated_user.ts (100%) rename services/platform/{convex => backend/core}/lib/rls/errors.ts (96%) rename services/platform/{convex => backend/core}/lib/rls/helpers/access_control.test.ts (100%) rename services/platform/{convex => backend/core}/lib/rls/helpers/access_control.ts (100%) rename services/platform/{convex => backend/core}/lib/rls/helpers/agent_read_access.ts (100%) rename services/platform/{convex => backend/core}/lib/rls/helpers/conversation_assignment.test.ts (100%) rename services/platform/{convex => backend/core}/lib/rls/helpers/conversation_assignment.ts (100%) rename services/platform/{convex => backend/core}/lib/rls/helpers/role_helpers.test.ts (100%) rename services/platform/{convex => backend/core}/lib/rls/helpers/role_helpers.ts (100%) rename services/platform/{convex => backend/core}/lib/rls/organization/get_organization_member.test.ts (100%) rename services/platform/{convex => backend/core}/lib/rls/organization/get_organization_member.ts (100%) rename services/platform/{convex => backend/core}/lib/rls/organization/get_user_organizations.test.ts (100%) rename services/platform/{convex => backend/core}/lib/rls/organization/get_user_organizations.ts (98%) rename services/platform/{convex => backend/core}/lib/rls/types.ts (100%) rename services/platform/{convex => backend/core}/lib/rows.ts (100%) rename services/platform/{convex => backend/core}/lib/safe_path_segment.ts (100%) rename services/platform/{convex => backend/core}/lib/search/index.ts (100%) rename services/platform/{convex => backend/core}/lib/search/listing_intent.test.ts (100%) rename services/platform/{convex => backend/core}/lib/search/listing_intent.ts (98%) rename services/platform/{convex => backend/core}/lib/search/relevance.test.ts (100%) rename services/platform/{convex => backend/core}/lib/search/relevance.ts (100%) rename services/platform/{convex => backend/core}/lib/search/run_entity_search.ts (100%) rename services/platform/{convex => backend/core}/lib/search/scoped_substring_search.ts (100%) rename services/platform/{convex => backend/core}/lib/search/strategies/contacts.ts (100%) rename services/platform/{convex => backend/core}/lib/search/strategies/documents.ts (100%) rename services/platform/{convex => backend/core}/lib/search/strategies/projects.ts (100%) rename services/platform/{convex => backend/core}/lib/search/strategies/tasks.ts (100%) rename services/platform/{convex => backend/core}/lib/search/types.ts (100%) rename services/platform/{convex => backend/core}/lib/secret_box.ts (100%) rename services/platform/{convex => backend/core}/lib/sops.ts (100%) rename services/platform/{convex => backend/core}/lib/storage/blob_access.ts (100%) rename services/platform/{convex => backend/core}/lib/storage/blob_delete.ts (100%) rename services/platform/{convex => backend/core}/lib/storage/blob_ref.test.ts (100%) rename services/platform/{convex => backend/core}/lib/storage/blob_ref.ts (100%) rename services/platform/{convex => backend/core}/lib/storage/browser_facing.test.ts (100%) rename services/platform/{convex => backend/core}/lib/storage/object_store.ts (100%) rename services/platform/{convex => backend/core}/lib/storage/sandbox_stage_token.ts (100%) rename services/platform/{convex => backend/core}/lib/team_access.ts (100%) rename services/platform/{convex => backend/core}/lib/types/pdfjs_worker.d.ts (100%) rename services/platform/{convex => backend/core}/lib/utils/client_ip.test.ts (100%) rename services/platform/{convex => backend/core}/lib/utils/client_ip.ts (100%) rename services/platform/{convex => backend/core}/lib/utils/sanitize_secrets.test.ts (100%) rename services/platform/{convex => backend/core}/lib/utils/sanitize_secrets.ts (100%) rename services/platform/{convex => backend/core}/login_attempts/helpers.test.ts (98%) rename services/platform/{convex => backend/core}/login_attempts/helpers.ts (97%) rename services/platform/{convex => backend/core}/members/mirror_sync.ts (100%) rename services/platform/{convex => backend/core}/members/types.ts (100%) rename services/platform/{convex => backend/core}/node_only/sandbox/connectors_bridge.test.ts (98%) rename services/platform/{convex => backend/core}/node_only/sandbox/connectors_bridge.ts (98%) rename services/platform/{convex => backend/core}/node_only/sandbox/engine_exec_runner.test.ts (100%) rename services/platform/{convex => backend/core}/node_only/sandbox/engine_exec_runner.ts (95%) rename services/platform/{convex => backend/core}/node_only/sandbox/gateway_provisioning.test.ts (100%) rename services/platform/{convex => backend/core}/node_only/sandbox/gateway_provisioning.ts (99%) rename services/platform/{convex => backend/core}/node_only/sandbox/helpers/session_client.test.ts (100%) rename services/platform/{convex => backend/core}/node_only/sandbox/helpers/session_client.ts (100%) rename services/platform/{convex => backend/core}/node_only/sandbox/helpers/stage_url.ts (100%) rename services/platform/{convex => backend/core}/node_only/sandbox/llm_gateway_admin.test.ts (100%) rename services/platform/{convex => backend/core}/node_only/sandbox/llm_gateway_admin.ts (99%) rename services/platform/{convex => backend/core}/node_only/sandbox/render_fetch.test.ts (100%) rename services/platform/{convex => backend/core}/node_only/sandbox/render_fetch.ts (100%) rename services/platform/{convex => backend/core}/node_only/sandbox/session_credentials.test.ts (100%) rename services/platform/{convex => backend/core}/node_only/sandbox/session_credentials.ts (100%) rename services/platform/{convex => backend/core}/node_only/sandbox/session_exec.test.ts (99%) rename services/platform/{convex => backend/core}/node_only/sandbox/session_exec.ts (100%) rename services/platform/{convex => backend/core}/node_only/sandbox/turn_equipment.ts (100%) rename services/platform/{convex => backend/core}/node_only/sandbox/workspace_domain_tools.ts (99%) rename services/platform/{convex => backend/core}/node_only/sandbox/workspace_tool_shared.ts (100%) rename services/platform/{convex => backend/core}/node_only/sandbox/workspace_tools_bridge.test.ts (99%) rename services/platform/{convex => backend/core}/node_only/sandbox/workspace_tools_bridge.ts (99%) rename services/platform/{convex => backend/core}/notifications/actionable_email_connectors.ts (100%) rename services/platform/{convex => backend/core}/notifications/actionable_email_input.ts (100%) rename services/platform/{convex => backend/core}/notifications/actor_name.ts (100%) rename services/platform/{convex => backend/core}/notifications/helpers.ts (100%) rename services/platform/{convex => backend/core}/notifications/notification_messages.test.ts (96%) rename services/platform/{convex => backend/core}/notifications/notification_messages.ts (99%) rename services/platform/{convex => backend/core}/notifications/personal_notification_url.ts (100%) rename services/platform/{convex => backend/core}/notifications/types.ts (100%) rename services/platform/{convex => backend/core}/object_storage/file_utils.ts (97%) rename services/platform/{convex => backend/core}/onedrive/derive_sync_targets.test.ts (100%) rename services/platform/{convex => backend/core}/onedrive/derive_sync_targets.ts (100%) rename services/platform/{convex => backend/core}/onedrive/get_file_metadata.ts (96%) rename services/platform/{convex => backend/core}/onedrive/import_files.test.ts (100%) rename services/platform/{convex => backend/core}/onedrive/import_files.ts (99%) rename services/platform/{convex => backend/core}/onedrive/list_files.ts (97%) rename services/platform/{convex => backend/core}/onedrive/list_folder_contents.test.ts (100%) rename services/platform/{convex => backend/core}/onedrive/list_folder_contents.ts (98%) rename services/platform/{convex => backend/core}/onedrive/list_sharepoint_drives.ts (97%) rename services/platform/{convex => backend/core}/onedrive/list_sharepoint_files.ts (98%) rename services/platform/{convex => backend/core}/onedrive/list_sharepoint_sites.ts (98%) rename services/platform/{convex => backend/core}/onedrive/reconcile_folder_sync.test.ts (100%) rename services/platform/{convex => backend/core}/onedrive/reconcile_folder_sync.ts (100%) rename services/platform/{convex => backend/core}/onedrive/refresh_token.ts (97%) rename services/platform/{convex => backend/core}/organizations/resolve_org_slug.test.ts (100%) rename services/platform/{convex => backend/core}/organizations/resolve_org_slug.ts (100%) rename services/platform/{convex => backend/core}/organizations/scaffold.ts (99%) rename services/platform/{convex => backend/core}/products/field_limits.test.ts (96%) rename services/platform/{convex => backend/core}/products/field_limits.ts (97%) rename services/platform/{convex => backend/core}/projects/access.test.ts (100%) rename services/platform/{convex => backend/core}/projects/access.ts (100%) rename services/platform/{convex => backend/core}/projects/audit_actions.ts (100%) rename services/platform/{convex => backend/core}/projects/resolve_project_access.ts (98%) rename services/platform/{convex => backend/core}/provider_credentials/broker_pool.test.ts (98%) rename services/platform/{convex => backend/core}/provider_credentials/broker_pool.ts (98%) rename services/platform/{convex => backend/core}/provider_credentials/masking.test.ts (100%) rename services/platform/{convex => backend/core}/provider_credentials/masking.ts (100%) rename services/platform/{convex => backend/core}/provider_credentials/resolve_credential.ts (98%) rename services/platform/{convex => backend/core}/provider_credentials/token_hash.test.ts (100%) rename services/platform/{convex => backend/core}/provider_credentials/token_hash.ts (100%) rename services/platform/{convex => backend/core}/provisioning/provision_default_automations.test.ts (98%) rename services/platform/{convex => backend/core}/provisioning/provision_default_automations.ts (96%) rename services/platform/{convex => backend/core}/sandbox/agent_deadline.test.ts (100%) rename services/platform/{convex => backend/core}/sandbox/agent_deadline.ts (100%) rename services/platform/{convex => backend/core}/sandbox/quota_policy.ts (96%) rename services/platform/{convex => backend/core}/sandbox/session_constants.ts (100%) rename services/platform/{convex => backend/core}/sandbox/session_naming.test.ts (100%) rename services/platform/{convex => backend/core}/sandbox/session_naming.ts (100%) rename services/platform/{convex => backend/core}/sandbox/tool_names.test.ts (100%) rename services/platform/{convex => backend/core}/sandbox/tool_names.ts (100%) rename services/platform/{convex => backend/core}/sandbox/user_env_constants.test.ts (100%) rename services/platform/{convex => backend/core}/sandbox/user_env_constants.ts (100%) rename services/platform/{convex => backend/core}/sandbox/workspace_access.ts (100%) rename services/platform/{convex => backend/core}/scim/data.ts (100%) rename services/platform/{convex => backend/core}/scim/discovery.ts (100%) rename services/platform/{convex => backend/core}/scim/helpers/crypto.ts (100%) rename services/platform/{convex => backend/core}/scim/http_actions.ts (100%) rename services/platform/{convex => backend/core}/scim/internal_mutations.ts (100%) rename services/platform/{convex => backend/core}/scim/links.ts (100%) rename services/platform/{convex => backend/core}/scim/mappers.test.ts (100%) rename services/platform/{convex => backend/core}/scim/mappers.ts (99%) rename services/platform/{convex => backend/core}/scim/responses.ts (100%) rename services/platform/{convex => backend/core}/scim/types.ts (100%) rename services/platform/{convex => backend/core}/skills/bundle_zip.test.ts (98%) rename services/platform/{convex => backend/core}/skills/bundle_zip.ts (97%) rename services/platform/{convex => backend/core}/skills/file_actions.test.ts (99%) rename services/platform/{convex => backend/core}/skills/file_actions.ts (98%) rename services/platform/{convex => backend/core}/skills/file_utils.test.ts (99%) rename services/platform/{convex => backend/core}/skills/file_utils.ts (99%) rename services/platform/{convex => backend/core}/skills/views.ts (96%) rename services/platform/{convex => backend/core}/tasks/access.test.ts (98%) rename services/platform/{convex => backend/core}/tasks/access.ts (97%) rename services/platform/{convex => backend/core}/tasks/agent_run_host.ts (99%) rename services/platform/{convex => backend/core}/tasks/audit_actions.ts (100%) rename services/platform/{convex => backend/core}/tasks/date_notification_recipients.test.ts (100%) rename services/platform/{convex => backend/core}/tasks/date_notification_recipients.ts (100%) rename services/platform/{convex => backend/core}/tasks/helpers.test.ts (100%) rename services/platform/{convex => backend/core}/tasks/helpers.ts (99%) rename services/platform/{convex => backend/core}/tasks/issue_ref.test.ts (100%) rename services/platform/{convex => backend/core}/tasks/issue_ref.ts (100%) rename services/platform/{convex => backend/core}/tasks/mentions.test.ts (100%) rename services/platform/{convex => backend/core}/tasks/mentions.ts (100%) rename services/platform/{convex => backend/core}/tasks/rank.test.ts (100%) rename services/platform/{convex => backend/core}/tasks/rank.ts (100%) rename services/platform/{convex => backend/core}/tasks/review_shared.ts (99%) rename services/platform/{convex => backend/core}/tasks/task_auto_retry.ts (100%) rename services/platform/{convex => backend/core}/tasks/task_kick_resume.test.ts (100%) rename services/platform/{convex => backend/core}/tasks/task_kick_resume.ts (100%) rename services/platform/{convex => backend/core}/tasks/task_serving.test.ts (99%) rename services/platform/{convex => backend/core}/tasks/task_serving.ts (100%) rename services/platform/{convex => backend/core}/tasks/types.ts (100%) rename services/platform/{convex => backend/core}/trusted_headers_auth/authenticate_handler.ts (100%) rename services/platform/{convex => backend/core}/tsconfig.json (100%) rename services/platform/{convex => backend/core}/tts/audio_mime.ts (100%) rename services/platform/{convex => backend/core}/tts/error_codes.ts (98%) rename services/platform/{convex => backend/core}/video_links/captions_parser.test.ts (100%) rename services/platform/{convex => backend/core}/video_links/captions_parser.ts (100%) rename services/platform/{convex => backend/core}/video_links/ingest_video_link.ts (99%) rename services/platform/{convex => backend/core}/video_links/internal_mutations.ts (100%) rename services/platform/{convex => backend/core}/video_links/url_safety.test.ts (100%) rename services/platform/{convex => backend/core}/video_links/url_safety.ts (97%) rename services/platform/{convex => backend/core}/video_links/ytdlp.test.ts (100%) rename services/platform/{convex => backend/core}/video_links/ytdlp.ts (100%) rename services/platform/{convex => backend/core}/video_links/ytdlp_toolchain.ts (100%) create mode 100644 services/platform/backend/core/webdav/README.md rename services/platform/{convex => backend/core}/webdav/SMOKE.md (100%) rename services/platform/{convex => backend/core}/webdav/helpers.ts (100%) rename services/platform/{convex => backend/core}/websites/create_website.ts (96%) rename services/platform/{convex => backend/core}/websites/internal_actions.ts (100%) rename services/platform/{convex => backend/core}/websites/match_website_search.ts (100%) rename services/platform/{convex => backend/core}/websites/scan_scheduling.test.ts (100%) rename services/platform/{convex => backend/core}/websites/scan_scheduling.ts (100%) rename services/platform/{convex => backend/core}/websites/types.ts (100%) rename services/platform/backend/lib/{convex-shim.ts => ctx-shim.ts} (92%) delete mode 100644 services/platform/convex/documents/extract_extension.ts delete mode 100644 services/platform/convex/webdav/README.md rename services/platform/{convex/lib/untrusted_content.test.ts => lib/chat/untrusted-content.test.ts} (98%) rename services/platform/{convex/lib/untrusted_content.ts => lib/chat/untrusted-content.ts} (98%) rename services/platform/{convex/lib/http/host_policy.ts => lib/net/host-policy.ts} (96%) rename services/platform/{convex/lib/http/safe_fetch.test.ts => lib/net/safe-fetch.test.ts} (96%) rename services/platform/{convex/lib/http/safe_fetch.ts => lib/net/safe-fetch.ts} (99%) rename services/platform/lib/shared/constants/{convex-enums.ts => product-enums.ts} (76%) rename services/platform/lib/utils/{convex-error.test.ts => backend-error.test.ts} (100%) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 320a9dbca8..1d8c399a90 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -133,7 +133,7 @@ jobs: # yt-dlp + deno + ffmpeg + the bgutil plugin are self-provisioned by the # live YouTube ingestion test via `ensureVideoToolchain()` in a - # `beforeAll` (convex/video_links/ytdlp_toolchain.ts). It still runs here + # `beforeAll` (backend/core/video_links/ytdlp_toolchain.ts). It still runs here # because GitHub runners are datacenter IPs — the environment where # YouTube's bot wall appears — so a green run proves the shipped anti-bot # stack actually gets past it. diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 74a129de47..441291d91e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -81,17 +81,15 @@ jobs: uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: services/platform/dist - key: e2e-dist-${{ runner.os }}-${{ hashFiles('services/platform/app/**', 'services/platform/lib/**', 'services/platform/convex/**', 'services/platform/public/**', 'services/platform/*.json', 'services/platform/*.ts', 'packages/ui/src/**', 'packages/ui/*.json', 'bun.lock') }} + key: e2e-dist-${{ runner.os }}-${{ hashFiles('services/platform/app/**', 'services/platform/lib/**', 'services/platform/backend/core/**', 'services/platform/public/**', 'services/platform/*.json', 'services/platform/*.ts', 'packages/ui/src/**', 'packages/ui/*.json', 'bun.lock') }} # Serve a production build instead of the Vite dev server. `vite dev` # transpiles on the fly — the dominant CPU consumer that, on the 4-vCPU - # runner, starved the local Convex backend into flooding its hard 1s - # function-execution timeout and flaking the suite. A prebuilt dist/ makes - # the webServer (scripts/dev.ts, TALE_E2E_SERVE_BUILD=1) serve via - # `vite preview`: static assets + Convex proxy only, no transpilation. + # runner, starved the backend into flaking the suite. A prebuilt dist/ + # makes the webServer (scripts/dev.ts, TALE_E2E_SERVE_BUILD=1) serve via + # `vite preview`: static assets + API proxy only, no transpilation. # Built here (not inside the webServer) so the boot stays inside the - # Playwright webServer timeout. The committed convex/_generated lets the - # client bundle build without a live backend. Skipped on a dist-cache hit. + # Playwright webServer timeout. Skipped on a dist-cache hit. - name: Build platform (prod bundle for E2E preview) if: steps.dist-cache.outputs.cache-hit != 'true' working-directory: services/platform diff --git a/compose.web.yml b/compose.web.yml index 0fdd9d6eb8..4595146330 100644 --- a/compose.web.yml +++ b/compose.web.yml @@ -2,7 +2,7 @@ # Tale Web (Marketing site) — Standalone Docker Compose # ============================================================================= # The marketing site at www.tale.dev runs independently of the platform stack -# (services/platform, convex, db, knowledge-db, proxy, sandbox) and has its own +# (services/platform, db, knowledge-db, proxy, sandbox) and has its own # environment file at services/web/.env.example. # # Usage: diff --git a/compose.yml b/compose.yml index fabfaef9a6..41a4ef9595 100644 --- a/compose.yml +++ b/compose.yml @@ -435,7 +435,7 @@ services: # Where the BROWSER reaches the store: the site origin, behind which the # proxy forwards `//*` verbatim. Presigned URLs are signed # against this, the internal endpoint above is what the backend itself - # uses — see `browserFacing` in convex/lib/storage/object_store.ts. + # uses — see `browserFacing` in backend/core/lib/storage/object_store.ts. OBJECT_STORE_PUBLIC_ENDPOINT: ${OBJECT_STORE_PUBLIC_ENDPOINT:-${SITE_URL:-http://localhost}} env_file: - path: .env @@ -500,7 +500,7 @@ services: # Where the BROWSER reaches the store: the site origin, behind which the # proxy forwards `//*` verbatim. Presigned URLs are signed # against this, the internal endpoint above is what the backend itself - # uses — see `browserFacing` in convex/lib/storage/object_store.ts. + # uses — see `browserFacing` in backend/core/lib/storage/object_store.ts. OBJECT_STORE_PUBLIC_ENDPOINT: ${OBJECT_STORE_PUBLIC_ENDPOINT:-${SITE_URL:-http://localhost}} env_file: - path: .env diff --git a/packages/ui/src/i18n/tests/checks/usage-missing.ts b/packages/ui/src/i18n/tests/checks/usage-missing.ts index db38649fd8..84520f595d 100644 --- a/packages/ui/src/i18n/tests/checks/usage-missing.ts +++ b/packages/ui/src/i18n/tests/checks/usage-missing.ts @@ -59,7 +59,7 @@ export function findMissingKeyRefs(config: MissingKeyRefsConfig): Finding[] { const { serviceRoot, messagesDir = path.join(serviceRoot, 'messages'), - scanRoots = ['app', 'components', 'hooks', 'lib', 'convex'], + scanRoots = ['app', 'components', 'hooks', 'lib', 'backend'], allowlistPath = path.join(serviceRoot, 'lib/i18n/keys-dynamic.yml'), baseFiles = ['en.yml', 'global.yml'], } = config; diff --git a/packages/ui/src/i18n/tests/config.ts b/packages/ui/src/i18n/tests/config.ts index 6599409286..71d9b582ca 100644 --- a/packages/ui/src/i18n/tests/config.ts +++ b/packages/ui/src/i18n/tests/config.ts @@ -74,7 +74,7 @@ export interface I18nTestsConfig { /** * Source roots scanned by the usage check for `t()` / `useT()` / dotted - * literals. Defaults to `['app', 'components', 'hooks', 'lib', 'convex']`; + * literals. Defaults to `['app', 'components', 'hooks', 'lib', 'backend']`; * missing roots are skipped without error. */ scanRoots?: string[]; diff --git a/packages/ui/src/i18n/tests/usage.ts b/packages/ui/src/i18n/tests/usage.ts index b0d190b0cf..ac48b75a6f 100644 --- a/packages/ui/src/i18n/tests/usage.ts +++ b/packages/ui/src/i18n/tests/usage.ts @@ -12,7 +12,7 @@ interface MessagesUsageConfig { /** * Top-level directories to scan for `t()` / `useT()` usage. Missing * directories are skipped. Defaults to `['app', 'components', 'hooks', - * 'lib', 'convex']` so the same list works across services with different + * 'lib', 'backend']` so the same list works across services with different * layouts. */ scanRoots?: string[]; @@ -460,7 +460,7 @@ export function defineMessagesUsageTests(config: MessagesUsageConfig): void { const { serviceRoot, messagesDir = path.join(serviceRoot, 'messages'), - scanRoots = ['app', 'components', 'hooks', 'lib', 'convex'], + scanRoots = ['app', 'components', 'hooks', 'lib', 'backend'], allowlistPath = path.join(serviceRoot, 'lib/i18n/keys-dynamic.yml'), baseFiles = ['en.yml', 'global.yml'], } = config; diff --git a/services/platform/.oxlintrc.json b/services/platform/.oxlintrc.json index 5707daa4c5..570d745751 100644 --- a/services/platform/.oxlintrc.json +++ b/services/platform/.oxlintrc.json @@ -45,7 +45,7 @@ } }, { - "files": ["convex/**/*.ts"], + "files": ["backend/core/**/*.ts"], "rules": { "unicorn/filename-case": [ "error", @@ -73,17 +73,6 @@ "typescript/no-unsafe-type-assertion": "off", "typescript/no-unnecessary-type-assertion": "off" } - }, - { - "files": [ - "convex/migrations/versions/**/*.ts", - "convex/migrations/framework/runner.ts", - "convex/migrations/framework/entrypoints.ts" - ], - "rules": { - "typescript/no-unsafe-type-assertion": "off", - "typescript/no-explicit-any": "off" - } } ], "ignorePatterns": [ diff --git a/services/platform/Dockerfile b/services/platform/Dockerfile index 890a09b400..dc54eba0ce 100644 --- a/services/platform/Dockerfile +++ b/services/platform/Dockerfile @@ -74,17 +74,17 @@ COPY services/platform/vite.config.ts \ ./services/platform/ COPY packages/ui ./packages/ui -# @tale/shared backs the convex logger + the knowledge config/db/utils re-export -# shims; convex deploy bundles its source at runtime, so the full source (not -# just package.json) must be present for the `@tale/shared/*` subpath imports to -# resolve. Mirrors the packages/ui copy above. +# @tale/shared is imported unbundled at runtime (`@tale/shared/*` subpaths), +# so the full source (not just package.json) must be present. Mirrors the +# packages/ui copy above. COPY packages/shared ./packages/shared COPY services/platform/app ./services/platform/app -# lib/agent-adapters lives here now (convex run_agent.ts bundles it at deploy -# time), so the lib copy below carries it into the builder. COPY services/platform/lib ./services/platform/lib -COPY services/platform/convex ./services/platform/convex +# The app imports type/constant vocabulary from `@/backend/core/**`, so the +# core tree must be present before `vite build`; the rest of backend/ is +# copied after the build (below) to keep the build cache narrow. +COPY services/platform/backend/core ./services/platform/backend/core COPY services/platform/public ./services/platform/public COPY services/platform/types ./services/platform/types COPY services/platform/messages ./services/platform/messages @@ -132,8 +132,8 @@ COPY services/platform/backend ./services/platform/backend # runner, this keeps the builder intact: full (unpruned) node_modules — vite + # the rolldown bundler + lightningcss + @parcel/watcher — and the complete # frontend source under /app/services/platform. That is what lets the entrypoint -# run `vite build --watch` (frontend hot reload) and `convex dev` (function hot -# push) against the host source bind-mounted by compose.dev.yml. The image ships +# run `vite build --watch` (frontend hot reload) against the host source +# bind-mounted by compose.dev.yml. The image ships # the already-built dist/ from the builder, so server.ts serves immediately # while the first watch rebuild runs in the background. # @@ -165,12 +165,6 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ && ln -sfn /app/node_modules /app/services/platform/node_modules \ && chmod +x /app/services/platform/docker-entrypoint.sh -# convex.json carries `node.externalPackages`; `convex deploy`/`convex dev` read -# it from the platform dir. Absent → heavy node-only libs (jsdom, …) get bundled -# inline and the push fails at analyze. (The runner copies it for the same -# reason.) -COPY services/platform/convex.json /app/services/platform/convex.json - # Override the builder's production defaults. These mirror the runner's runtime # ENV (so the entrypoint behaves identically) except NODE_ENV, which gates the # dev hot-reload watchers, and TALE_DEV_NO_PRIVDROP, which keeps the container @@ -179,9 +173,7 @@ ENV NODE_ENV=development \ TALE_VERSION=${VERSION} \ PORT=3000 \ HOSTNAME="0.0.0.0" \ - CONVEX_URL=http://convex:3210 \ SANDBOX_STORAGE_INTERNAL_BASE_URL=http://convex:3210 \ - INSTANCE_NAME=tale_platform \ DO_NOT_TRACK=1 \ TALE_CONFIG_DIR=/app/data \ TALE_CONFIG_BUILTIN_DIR=/app/builtin \ @@ -366,7 +358,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ ARG VERSION=dev LABEL org.opencontainers.image.version="${VERSION}" \ org.opencontainers.image.title="tale-platform" \ - org.opencontainers.image.description="Tale Platform — TanStack Start SPA + Convex Backend" \ + org.opencontainers.image.description="Tale Platform — TanStack Start SPA + Postgres backend" \ org.opencontainers.image.source="https://github.com/tale-project/tale" \ org.opencontainers.image.vendor="Tale" \ org.opencontainers.image.licenses="MIT" @@ -375,45 +367,26 @@ ENV NODE_ENV=production \ TALE_VERSION=${VERSION} \ PORT=3000 \ HOSTNAME="0.0.0.0" \ - # Convex service DNS name (compose-internal). Overridable via CONVEX_URL. - CONVEX_URL=http://convex:3210 \ - # Origin that the sandbox spawner uses to POST presigned-URL output - # uploads back to Convex. Read by Convex Node actions via process.env - # in toSandboxStorageUrl() (see convex/lib/helpers/public_storage_url.ts). - # Node actions only see vars that this container's entrypoint pushes - # into Convex's deployment env via `convex env set`, so baking the - # value into the platform image is what guarantees the rewrite has - # a reachable origin on every docker deploy. Direct to convex:3210 - # rather than the Caddy proxy because Caddy is HTTPS-only with a - # self-signed cert and would 308-redirect plain HTTP POSTs. + # Origin the in-sandbox daemons use to fetch staged blobs — the + # sandbox-net alias, read via process.env in toSandboxStorageUrl() + # (see backend/core/lib/helpers/public_storage_url.ts). Baked so the + # rewrite has a reachable origin on every docker deploy. Direct rather + # than the Caddy proxy because Caddy is HTTPS-only with a self-signed + # cert and would 308-redirect plain HTTP POSTs. SANDBOX_STORAGE_INTERNAL_BASE_URL=http://convex:3210 \ - # INSTANCE_NAME is shared with convex service; platform uses it + INSTANCE_SECRET - # to compute the admin key for `bunx convex env set` and `bunx convex deploy`. - INSTANCE_NAME=tale_platform \ DO_NOT_TRACK=1 \ - # Semantic value of the file-config parent path inside the convex - # container. Platform forces this at push time in docker-entrypoint.sh - # (to tombstone any stale host-side `.env` value). Under the org-first - # layout, every per-domain config dir is derived as - # $TALE_CONFIG_DIR/// — e.g. - # /app/data/default/agents/, /app/data/default/providers/, etc. - # The previous per-domain env vars (AGENTS_DIR, …) are no longer - # honored; the entrypoint actively purges them from Convex on every - # boot. + # The file-config parent path. Under the org-first layout, every + # per-domain config dir is derived as $TALE_CONFIG_DIR/// + # — e.g. /app/data/default/agents/, /app/data/default/providers/, etc. TALE_CONFIG_DIR=/app/data \ - # Read-only builtin catalog baked into the convex image (see - # services/convex/Dockerfile). Declared here because Convex Node - # actions only see env vars that this container pushes to Convex's - # deployment env via the entrypoint's `convex env set` loop — even - # though the path points at files inside the *convex* container. - # Generic catalog — its children ARE the domains - # ($TALE_CONFIG_BUILTIN_DIR//), with no org level and no fallback. - # Every org is seeded from it. See convex/organizations/scaffold.ts. + # Read-only builtin catalog baked into this image (COPY below). Generic + # catalog — its children ARE the domains ($TALE_CONFIG_BUILTIN_DIR//), + # with no org level and no fallback. Every org is seeded from it. See + # backend/core/organizations/scaffold.ts. TALE_CONFIG_BUILTIN_DIR=/app/builtin \ # Org-independent system config (providers/models/harnesses/connectors), - # read by 'use node' actions. Same push-to-Convex-env path as above; a - # container has no repo checkout to fall back on. Baked at /app/system in - # the convex image (services/convex/Dockerfile). + # baked at /app/system below; a container has no repo checkout to fall + # back on. TALE_CONFIG_SYSTEM_DIR=/app/system \ # Where the corpus DDL lives in this image (copied below). Set # explicitly because `findMigrationsDir`'s fallback walks up the module @@ -428,11 +401,6 @@ COPY --from=pruner --chown=app:app \ /app/services/platform/sla-targets.ts \ /app/services/platform/status-probe.ts \ ./ -COPY --from=pruner --chown=app:app /app/services/platform/convex ./convex -# convex.json carries `node.externalPackages`; the runtime `convex deploy` (run -# from /app) reads it from /app/convex.json. Without it the heavy node-only libs -# (jsdom, …) get bundled inline and the push fails at analyze. -COPY --chown=app:app services/platform/convex.json ./convex.json COPY --from=pruner --chown=app:app /app/services/platform/lib ./lib # 0.5 backend sources (run unbundled by Node via type stripping; role dispatch # in docker-entrypoint.sh execs `node /app/backend/main.ts`). @@ -494,9 +462,7 @@ ENV NODE_ENV=production \ TALE_VERSION=${VERSION} \ PORT=3000 \ HOSTNAME="0.0.0.0" \ - CONVEX_URL=http://convex:3210 \ SANDBOX_STORAGE_INTERNAL_BASE_URL=http://convex:3210 \ - INSTANCE_NAME=tale_platform \ DO_NOT_TRACK=1 \ TALE_CONFIG_DIR=/app/data \ TALE_CONFIG_BUILTIN_DIR=/app/builtin \ diff --git a/services/platform/Dockerfile.dockerignore b/services/platform/Dockerfile.dockerignore index b8bf05b0fd..3a3bdfc859 100644 --- a/services/platform/Dockerfile.dockerignore +++ b/services/platform/Dockerfile.dockerignore @@ -127,7 +127,7 @@ services/db/ !services/db/package.json # The knowledge corpus migrations ship IN the image: a new BYO corpus is # prepared at runtime from these files, and without them that degrades to a -# "apply them yourself" notice (the guard test in convex/knowledge pins both +# "apply them yourself" notice (the guard test in backend/core/knowledge pins both # the COPY and the ENV). !services/db/migrations/ !services/db/migrations/knowledge-db/** diff --git a/services/platform/app/features/automations/components/agent-node-fields.tsx b/services/platform/app/features/automations/components/agent-node-fields.tsx index 6b137865ca..bce7273598 100644 --- a/services/platform/app/features/automations/components/agent-node-fields.tsx +++ b/services/platform/app/features/automations/components/agent-node-fields.tsx @@ -33,7 +33,7 @@ import { findSelectedModel, toModelOptions, } from '@/app/features/projects/lib/model-options'; -import { AGENT_TOOL_CATALOG } from '@/convex/sandbox/tool_names'; +import { AGENT_TOOL_CATALOG } from '@/backend/core/sandbox/tool_names'; import type { NodeDef } from '@/lib/engine/core/types'; import { useT } from '@/lib/i18n/client'; diff --git a/services/platform/app/features/automations/components/blank-automation-dialog.tsx b/services/platform/app/features/automations/components/blank-automation-dialog.tsx index ddc074e16b..bdb942a642 100644 --- a/services/platform/app/features/automations/components/blank-automation-dialog.tsx +++ b/services/platform/app/features/automations/components/blank-automation-dialog.tsx @@ -37,8 +37,8 @@ import { toModelOptions, } from '@/app/features/projects/lib/model-options'; import { toast } from '@/app/hooks/use-toast'; -import { EVENT_TYPES } from '@/convex/events/emit'; -import { AGENT_TOOL_CATALOG } from '@/convex/sandbox/tool_names'; +import { EVENT_TYPES } from '@/backend/core/events/emit'; +import { AGENT_TOOL_CATALOG } from '@/backend/core/sandbox/tool_names'; import { automationSlugToParam } from '@/lib/automations/slug'; import { useT } from '@/lib/i18n/client'; diff --git a/services/platform/app/features/automations/components/trigger-editor.tsx b/services/platform/app/features/automations/components/trigger-editor.tsx index 907ddbdbb8..e7c6bf22ce 100644 --- a/services/platform/app/features/automations/components/trigger-editor.tsx +++ b/services/platform/app/features/automations/components/trigger-editor.tsx @@ -13,7 +13,7 @@ import { ConfirmDialog } from '@/app/components/ui/dialog/confirm-dialog'; import { Select } from '@/app/components/ui/forms/select'; import { Switch } from '@/app/components/ui/forms/switch'; import { useFormatDate } from '@/app/hooks/use-format-date'; -import { EVENT_TYPES } from '@/convex/events/emit'; +import { EVENT_TYPES } from '@/backend/core/events/emit'; import { useT } from '@/lib/i18n/client'; import { diff --git a/services/platform/app/features/chat/components/chat-surface.test.tsx b/services/platform/app/features/chat/components/chat-surface.test.tsx index 733008e53f..20558b1fe5 100644 --- a/services/platform/app/features/chat/components/chat-surface.test.tsx +++ b/services/platform/app/features/chat/components/chat-surface.test.tsx @@ -69,8 +69,8 @@ vi.mock( // The image-attachment upload lane talks to Convex (upload handoff, policy // read, file-metadata registration) — none of which exists here. An inert // stand-in keeps the composer's attach surface mounted with nothing staged. -vi.mock('@/app/features/shared/files/use-convex-file-upload', () => ({ - useConvexFileUpload: () => ({ +vi.mock('@/app/features/shared/files/use-file-upload', () => ({ + useFileUpload: () => ({ attachments: [], setAttachments: vi.fn(), uploadingFiles: [], diff --git a/services/platform/app/features/chat/components/chat-surface.tsx b/services/platform/app/features/chat/components/chat-surface.tsx index c3bf94840e..6d7fabb737 100644 --- a/services/platform/app/features/chat/components/chat-surface.tsx +++ b/services/platform/app/features/chat/components/chat-surface.tsx @@ -50,7 +50,7 @@ import { Sheet } from '@/app/components/ui/overlays/sheet'; import { DataNoticeFooter } from '@/app/features/governance/components/data-notice-footer'; import { useMyBudgetStatus } from '@/app/features/settings/governance/hooks/queries'; import { useUploadPolicy } from '@/app/features/settings/governance/hooks/queries'; -import { useConvexFileUpload } from '@/app/features/shared/files/use-convex-file-upload'; +import { useFileUpload } from '@/app/features/shared/files/use-file-upload'; import { freezeActiveStream, resetGlobalFreeze, @@ -835,7 +835,7 @@ function ChatSurfaceInner({ }), [organizationId, threadId], ); - const attachmentUpload = useConvexFileUpload(uploadConfig); + const attachmentUpload = useFileUpload(uploadConfig); // The picker's `accept` filter mirrors 0.3's `effectiveAccept`: the org // upload policy's extension list when one is enforced, else the full // chat family. Validation happens in the upload hook either way. diff --git a/services/platform/app/features/chat/components/composer-attachments.tsx b/services/platform/app/features/chat/components/composer-attachments.tsx index b4c4bb1454..4332ce8e8c 100644 --- a/services/platform/app/features/chat/components/composer-attachments.tsx +++ b/services/platform/app/features/chat/components/composer-attachments.tsx @@ -28,7 +28,7 @@ import type { FileTranscriptionInfo } from '@/app/features/chat/hooks/use-file-t import type { FileAttachment } from '@/app/features/shared/files/types'; import { useFileUrl } from '@/app/features/shared/files/use-file-url'; import { ImagePreviewDialog } from '@/app/features/shared/markdown/image-preview-dialog'; -import type { BlobRef } from '@/convex/lib/storage/blob_ref'; +import type { BlobRef } from '@/backend/core/lib/storage/blob_ref'; import { useT } from '@/lib/i18n/client'; import { isAudioOrVideo, isImage } from '@/lib/shared/file-types'; import { formatFileSize } from '@/lib/utils/format/file'; diff --git a/services/platform/app/features/chat/components/composer.tsx b/services/platform/app/features/chat/components/composer.tsx index b00f3d43b0..0429b15e7a 100644 --- a/services/platform/app/features/chat/components/composer.tsx +++ b/services/platform/app/features/chat/components/composer.tsx @@ -44,7 +44,7 @@ import { extractPastedImageFiles } from '@/app/features/shared/files/clipboard-i import type { FileAttachment } from '@/app/features/shared/files/types'; import { usePersistedState } from '@/app/hooks/use-persisted-state'; import { toast } from '@/app/hooks/use-toast'; -import type { BlobRef } from '@/convex/lib/storage/blob_ref'; +import type { BlobRef } from '@/backend/core/lib/storage/blob_ref'; import { useT } from '@/lib/i18n/client'; import { CHAT_UPLOAD_ACCEPT } from '@/lib/shared/file-types'; diff --git a/services/platform/app/features/chat/hooks/use-file-indexing-status.ts b/services/platform/app/features/chat/hooks/use-file-indexing-status.ts index 2f9b3d21ec..f84f4f058f 100644 --- a/services/platform/app/features/chat/hooks/use-file-indexing-status.ts +++ b/services/platform/app/features/chat/hooks/use-file-indexing-status.ts @@ -6,7 +6,7 @@ import { useEffect, useMemo, useRef } from 'react'; import type { FileAttachment } from '@/app/features/shared/files/types'; import { toast } from '@/app/hooks/use-toast'; import { fileStatusesQuery } from '@/app/lib/backend/chat'; -import type { BlobRef } from '@/convex/lib/storage/blob_ref'; +import type { BlobRef } from '@/backend/core/lib/storage/blob_ref'; import { useT } from '@/lib/i18n/client'; import { isAudioOrVideo, @@ -30,7 +30,7 @@ export interface FileIndexingInfo { /** * RAG-indexing status for document / text attachments staged in the * composer — the set the upload hook defers its success toast for - * (`willIndex` in `use-convex-file-upload`). Reactive Convex query; the + * (`willIndex` in `use-file-upload`). Reactive Convex query; the * server-side poll chain patches the row as ingestion progresses and the * watchdog guarantees a terminal state, so no client polling is needed. * diff --git a/services/platform/app/features/chat/hooks/use-file-transcription-status.ts b/services/platform/app/features/chat/hooks/use-file-transcription-status.ts index dca9fd32e3..dfa74208e6 100644 --- a/services/platform/app/features/chat/hooks/use-file-transcription-status.ts +++ b/services/platform/app/features/chat/hooks/use-file-transcription-status.ts @@ -5,7 +5,7 @@ import { useMemo } from 'react'; import type { FileAttachment } from '@/app/features/shared/files/types'; import { fileStatusesQuery } from '@/app/lib/backend/chat'; -import type { BlobRef } from '@/convex/lib/storage/blob_ref'; +import type { BlobRef } from '@/backend/core/lib/storage/blob_ref'; import { isAudioOrVideo } from '@/lib/shared/file-types'; import { useChatQueryClient } from '../data/chat-backend'; diff --git a/services/platform/app/features/chat/utils/voice-error-messages.ts b/services/platform/app/features/chat/utils/voice-error-messages.ts index 011dd7502e..d118d225c6 100644 --- a/services/platform/app/features/chat/utils/voice-error-messages.ts +++ b/services/platform/app/features/chat/utils/voice-error-messages.ts @@ -8,7 +8,7 @@ * pull it without dragging the other's render path. * * Code coverage: - * - Server-classified codes from `convex/tts/error_codes.ts` + * - Server-classified codes from `backend/core/tts/error_codes.ts` * (NO_PROVIDER, UNKNOWN_*, RATE_LIMITED, BUDGET_EXCEEDED, TIMEOUT, * PROVIDER_*, PROVIDER_INVALID_RESPONSE, HOST_POLICY, * MESSAGE_CHAR_LIMIT, CONTENTION, WATCHDOG_TIMEOUT, diff --git a/services/platform/app/features/contacts/components/contact-info-dialog.tsx b/services/platform/app/features/contacts/components/contact-info-dialog.tsx index 0a2746da2f..7ae83ec3e3 100644 --- a/services/platform/app/features/contacts/components/contact-info-dialog.tsx +++ b/services/platform/app/features/contacts/components/contact-info-dialog.tsx @@ -9,7 +9,7 @@ import { useCallback, useState } from 'react'; import { ViewDialog } from '@/app/components/ui/dialog/view-dialog'; import { useAbility } from '@/app/hooks/use-ability'; import type { ContactDoc } from '@/app/lib/backend/contract/docs'; -import type { ContactInfo } from '@/convex/conversations/types'; +import type { ContactInfo } from '@/backend/core/conversations/types'; import { useT } from '@/lib/i18n/client'; import { isContactDoc, UNKNOWN_CONTACT_EMAIL } from '../lib/contact-data'; diff --git a/services/platform/app/features/contacts/lib/contact-data.ts b/services/platform/app/features/contacts/lib/contact-data.ts index 6fa54b8af0..12f50d6691 100644 --- a/services/platform/app/features/contacts/lib/contact-data.ts +++ b/services/platform/app/features/contacts/lib/contact-data.ts @@ -1,5 +1,5 @@ import type { ContactDoc } from '@/app/lib/backend/contract/docs'; -import type { ContactInfo } from '@/convex/conversations/types'; +import type { ContactInfo } from '@/backend/core/conversations/types'; import { formatEnumLabel } from '@/lib/utils/string'; /** diff --git a/services/platform/app/features/conversations/components/conversation-header.tsx b/services/platform/app/features/conversations/components/conversation-header.tsx index 0f79604b78..88b6cd4aa0 100644 --- a/services/platform/app/features/conversations/components/conversation-header.tsx +++ b/services/platform/app/features/conversations/components/conversation-header.tsx @@ -25,7 +25,7 @@ import { toast } from '@/app/hooks/use-toast'; import { mailboxSideAddress, resolveReplyFrom, -} from '@/convex/conversations/reply_from'; +} from '@/backend/core/conversations/reply_from'; import { useT } from '@/lib/i18n/client'; import { isRecord } from '@/lib/utils/type-utils'; diff --git a/services/platform/app/features/conversations/components/conversations-navigation.tsx b/services/platform/app/features/conversations/components/conversations-navigation.tsx index 08dd6bb7ca..1e9af74926 100644 --- a/services/platform/app/features/conversations/components/conversations-navigation.tsx +++ b/services/platform/app/features/conversations/components/conversations-navigation.tsx @@ -6,7 +6,7 @@ import { TabNavigation, type TabNavigationItem, } from '@/app/components/ui/navigation/tab-navigation'; -import { DEFAULT_COUNT_CAP } from '@/convex/lib/helpers/count_items_in_org'; +import { DEFAULT_COUNT_CAP } from '@/backend/core/lib/helpers/count_items_in_org'; import { useT } from '@/lib/i18n/client'; import { useApproxConversationCountByStatus } from '../hooks/queries'; diff --git a/services/platform/app/features/conversations/components/conversations.test.tsx b/services/platform/app/features/conversations/components/conversations.test.tsx index 13715be08c..80310bd1ca 100644 --- a/services/platform/app/features/conversations/components/conversations.test.tsx +++ b/services/platform/app/features/conversations/components/conversations.test.tsx @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import type { UsePaginatedQueryReturnType } from '@/app/hooks/use-cached-paginated-query'; -import type { ConversationItem } from '@/convex/conversations/types'; +import type { ConversationItem } from '@/backend/core/conversations/types'; import { render, screen } from '@/tests/utils/render'; import { Conversations } from './conversations'; diff --git a/services/platform/app/features/conversations/components/conversations.tsx b/services/platform/app/features/conversations/components/conversations.tsx index 3a8be3ae11..422c00aad0 100644 --- a/services/platform/app/features/conversations/components/conversations.tsx +++ b/services/platform/app/features/conversations/components/conversations.tsx @@ -21,7 +21,7 @@ import { Checkbox } from '@/app/components/ui/forms/checkbox'; import { SearchInput } from '@/app/components/ui/forms/search-input'; import { Tooltip } from '@/app/components/ui/overlays/tooltip'; import type { UsePaginatedQueryReturnType } from '@/app/hooks/use-cached-paginated-query'; -import type { ConversationItem } from '@/convex/conversations/types'; +import type { ConversationItem } from '@/backend/core/conversations/types'; import { useT } from '@/lib/i18n/client'; import { cn } from '@/lib/utils/cn'; import { filterByTextSearch } from '@/lib/utils/filtering'; diff --git a/services/platform/app/features/conversations/hooks/use-bulk-actions.test.ts b/services/platform/app/features/conversations/hooks/use-bulk-actions.test.ts index bd44e817bc..182af077ba 100644 --- a/services/platform/app/features/conversations/hooks/use-bulk-actions.test.ts +++ b/services/platform/app/features/conversations/hooks/use-bulk-actions.test.ts @@ -2,7 +2,7 @@ import { act, renderHook } from '@testing-library/react'; import { afterEach, beforeEach, describe, it, expect, vi } from 'vitest'; -import type { ConversationItem } from '@/convex/conversations/types'; +import type { ConversationItem } from '@/backend/core/conversations/types'; import type { SelectionState } from '../types/selection'; diff --git a/services/platform/app/features/conversations/hooks/use-bulk-actions.ts b/services/platform/app/features/conversations/hooks/use-bulk-actions.ts index edb87d22e1..c501d5d060 100644 --- a/services/platform/app/features/conversations/hooks/use-bulk-actions.ts +++ b/services/platform/app/features/conversations/hooks/use-bulk-actions.ts @@ -1,7 +1,7 @@ import { useState, useCallback } from 'react'; import { toast } from '@/app/hooks/use-toast'; -import type { ConversationItem } from '@/convex/conversations/types'; +import type { ConversationItem } from '@/backend/core/conversations/types'; import { useT } from '@/lib/i18n/client'; import type { SelectionState } from '../types/selection'; diff --git a/services/platform/app/features/conversations/hooks/use-conversation-selection.test.ts b/services/platform/app/features/conversations/hooks/use-conversation-selection.test.ts index 5adb6c9502..42c6a92c52 100644 --- a/services/platform/app/features/conversations/hooks/use-conversation-selection.test.ts +++ b/services/platform/app/features/conversations/hooks/use-conversation-selection.test.ts @@ -1,7 +1,7 @@ import { act, renderHook } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; -import type { ConversationItem } from '@/convex/conversations/types'; +import type { ConversationItem } from '@/backend/core/conversations/types'; import { useConversationSelection } from './use-conversation-selection'; diff --git a/services/platform/app/features/conversations/hooks/use-conversation-selection.ts b/services/platform/app/features/conversations/hooks/use-conversation-selection.ts index 44cd0bda3d..aa8131e036 100644 --- a/services/platform/app/features/conversations/hooks/use-conversation-selection.ts +++ b/services/platform/app/features/conversations/hooks/use-conversation-selection.ts @@ -1,6 +1,6 @@ import { useState, useMemo, useCallback } from 'react'; -import type { ConversationItem } from '@/convex/conversations/types'; +import type { ConversationItem } from '@/backend/core/conversations/types'; import type { SelectionState } from '../types/selection'; diff --git a/services/platform/app/features/conversations/lib/email-connectors.ts b/services/platform/app/features/conversations/lib/email-connectors.ts index 6c6fef6fcc..0342a5372e 100644 --- a/services/platform/app/features/conversations/lib/email-connectors.ts +++ b/services/platform/app/features/conversations/lib/email-connectors.ts @@ -13,7 +13,7 @@ * would refuse. */ -import { isPublicEmailDomain } from '@/convex/conversations/reply_from'; +import { isPublicEmailDomain } from '@/backend/core/conversations/reply_from'; /** A connected, email-capable inbox the compose dialog can send through. */ export interface EmailConnectorOption { diff --git a/services/platform/app/features/documents/components/document-history-dialog.tsx b/services/platform/app/features/documents/components/document-history-dialog.tsx index f230a1b2da..1b46ca31ce 100644 --- a/services/platform/app/features/documents/components/document-history-dialog.tsx +++ b/services/platform/app/features/documents/components/document-history-dialog.tsx @@ -13,7 +13,7 @@ import { ComparisonResults } from '@/app/features/documents/components/document- import { useDocumentComparison } from '@/app/features/documents/hooks/use-document-comparison'; import { useFormatDate } from '@/app/hooks/use-format-date'; import { toast } from '@/app/hooks/use-toast'; -import type { BlobRef } from '@/convex/lib/storage/blob_ref'; +import type { BlobRef } from '@/backend/core/lib/storage/blob_ref'; import { useT } from '@/lib/i18n/client'; import { cn } from '@/lib/utils/cn'; diff --git a/services/platform/app/features/documents/components/rag-status-badge.tsx b/services/platform/app/features/documents/components/rag-status-badge.tsx index 3ecc0239ac..73278c6e4b 100644 --- a/services/platform/app/features/documents/components/rag-status-badge.tsx +++ b/services/platform/app/features/documents/components/rag-status-badge.tsx @@ -10,7 +10,7 @@ import { ViewDialog } from '@/app/components/ui/dialog/view-dialog'; import { useAbility } from '@/app/hooks/use-ability'; import { useFormatDate } from '@/app/hooks/use-format-date'; import { toast } from '@/app/hooks/use-toast'; -import { RAG_ERROR_EMBEDDING_NOT_CONFIGURED } from '@/convex/knowledge/rag_error_codes'; +import { RAG_ERROR_EMBEDDING_NOT_CONFIGURED } from '@/backend/core/knowledge/rag_error_codes'; import { useT } from '@/lib/i18n/client'; import type { RagStatus } from '@/types/documents'; diff --git a/services/platform/app/features/knowledge-entries/components/knowledge-entry-add-dialog.tsx b/services/platform/app/features/knowledge-entries/components/knowledge-entry-add-dialog.tsx index bb53681438..17b51d8061 100644 --- a/services/platform/app/features/knowledge-entries/components/knowledge-entry-add-dialog.tsx +++ b/services/platform/app/features/knowledge-entries/components/knowledge-entry-add-dialog.tsx @@ -12,7 +12,7 @@ import { toast } from '@/app/hooks/use-toast'; import { CONTENT_MAX_LENGTH, TOPIC_MAX_LENGTH, -} from '@/convex/knowledge_entries/constants'; +} from '@/backend/core/knowledge_entries/constants'; import { useT } from '@/lib/i18n/client'; import { backendErrorCode } from '@/lib/utils/backend-error'; diff --git a/services/platform/app/features/knowledge-entries/components/knowledge-entry-edit-dialog.tsx b/services/platform/app/features/knowledge-entries/components/knowledge-entry-edit-dialog.tsx index eb05062bf7..7b3700ebe5 100644 --- a/services/platform/app/features/knowledge-entries/components/knowledge-entry-edit-dialog.tsx +++ b/services/platform/app/features/knowledge-entries/components/knowledge-entry-edit-dialog.tsx @@ -12,7 +12,7 @@ import { toast } from '@/app/hooks/use-toast'; import { CONTENT_MAX_LENGTH, TOPIC_MAX_LENGTH, -} from '@/convex/knowledge_entries/constants'; +} from '@/backend/core/knowledge_entries/constants'; import { useT } from '@/lib/i18n/client'; import { backendErrorCode } from '@/lib/utils/backend-error'; diff --git a/services/platform/app/features/products/components/product-create-dialog.tsx b/services/platform/app/features/products/components/product-create-dialog.tsx index 4c00505e48..840a329792 100644 --- a/services/platform/app/features/products/components/product-create-dialog.tsx +++ b/services/platform/app/features/products/components/product-create-dialog.tsx @@ -24,12 +24,12 @@ import { PRODUCT_DESCRIPTION_MAX, PRODUCT_IMAGE_URL_MAX, PRODUCT_NAME_MAX, -} from '@/convex/products/field_limits'; +} from '@/backend/core/products/field_limits'; import { useT } from '@/lib/i18n/client'; import { PRODUCT_STATUS, type ProductStatus, -} from '@/lib/shared/constants/convex-enums'; +} from '@/lib/shared/constants/product-enums'; import { useCreateProduct } from '../hooks/mutations'; import { ProductImageField } from './product-image-field'; diff --git a/services/platform/app/features/products/components/product-edit-dialog.tsx b/services/platform/app/features/products/components/product-edit-dialog.tsx index 864f00d883..fab8c4589a 100644 --- a/services/platform/app/features/products/components/product-edit-dialog.tsx +++ b/services/platform/app/features/products/components/product-edit-dialog.tsx @@ -18,7 +18,7 @@ import { PRODUCT_DESCRIPTION_MAX, PRODUCT_IMAGE_URL_MAX, PRODUCT_NAME_MAX, -} from '@/convex/products/field_limits'; +} from '@/backend/core/products/field_limits'; import { useT } from '@/lib/i18n/client'; import { useUpdateProduct } from '../hooks/mutations'; diff --git a/services/platform/app/features/products/components/products-import-dialog.tsx b/services/platform/app/features/products/components/products-import-dialog.tsx index 434b49f3cf..f0f950ddd6 100644 --- a/services/platform/app/features/products/components/products-import-dialog.tsx +++ b/services/platform/app/features/products/components/products-import-dialog.tsx @@ -14,8 +14,8 @@ import { } from '@/app/hooks/use-file-import'; import { toast } from '@/app/hooks/use-toast'; import { useT } from '@/lib/i18n/client'; -import type { ProductStatus } from '@/lib/shared/constants/convex-enums'; -import { PRODUCT_STATUS } from '@/lib/shared/constants/convex-enums'; +import type { ProductStatus } from '@/lib/shared/constants/product-enums'; +import { PRODUCT_STATUS } from '@/lib/shared/constants/product-enums'; import { useBulkCreateProducts } from '../hooks/mutations'; import { ProductImportForm } from './product-import-form'; diff --git a/services/platform/app/features/projects/components/project-agent-dialog.tsx b/services/platform/app/features/projects/components/project-agent-dialog.tsx index 779deca474..04ebfe2969 100644 --- a/services/platform/app/features/projects/components/project-agent-dialog.tsx +++ b/services/platform/app/features/projects/components/project-agent-dialog.tsx @@ -21,7 +21,7 @@ import { SearchableSelect } from '@/app/components/ui/forms/searchable-select'; import { Select } from '@/app/components/ui/forms/select'; import { Textarea } from '@/app/components/ui/forms/textarea'; import { toast } from '@/app/hooks/use-toast'; -import { AGENT_TOOL_CATALOG } from '@/convex/sandbox/tool_names'; +import { AGENT_TOOL_CATALOG } from '@/backend/core/sandbox/tool_names'; import { useT } from '@/lib/i18n/client'; import { AppError } from '@/lib/shared/errors/app-error'; diff --git a/services/platform/app/features/settings/audit-logs/hooks/queries.ts b/services/platform/app/features/settings/audit-logs/hooks/queries.ts index 03f8f1e698..9fb5ad49ad 100644 --- a/services/platform/app/features/settings/audit-logs/hooks/queries.ts +++ b/services/platform/app/features/settings/audit-logs/hooks/queries.ts @@ -1,6 +1,6 @@ import { useBackendQuery } from '@/app/hooks/use-backend-query'; import { useCachedPaginatedQuery } from '@/app/hooks/use-cached-paginated-query'; -import type { AuditLogFilter } from '@/convex/audit_logs/types'; +import type { AuditLogFilter } from '@/backend/core/audit_logs/types'; export function useListAuditLogs( organizationId: string, diff --git a/services/platform/app/features/settings/governance/components/trash-page.tsx b/services/platform/app/features/settings/governance/components/trash-page.tsx index 5980adeace..1f311c0a9b 100644 --- a/services/platform/app/features/settings/governance/components/trash-page.tsx +++ b/services/platform/app/features/settings/governance/components/trash-page.tsx @@ -19,7 +19,7 @@ import { useToast } from '@/app/hooks/use-toast'; import { SOFT_DELETE_RESOURCE_TYPES, type SoftDeleteResourceType, -} from '@/convex/governance/soft_delete'; +} from '@/backend/core/governance/soft_delete'; import { useT } from '@/lib/i18n/client'; import { mapGovernanceSaveError } from '../governance-save-errors'; diff --git a/services/platform/app/features/settings/governance/components/voice-output-policy-editor.tsx b/services/platform/app/features/settings/governance/components/voice-output-policy-editor.tsx index 0fd9079960..70c4988874 100644 --- a/services/platform/app/features/settings/governance/components/voice-output-policy-editor.tsx +++ b/services/platform/app/features/settings/governance/components/voice-output-policy-editor.tsx @@ -19,7 +19,7 @@ interface VoiceOutputPolicyEditorProps { } // Backend default is ON when the policy row is missing (see -// `isVoiceOutputOrgEnabled` in convex/tts/queries.ts). Mirror that here so +// `isVoiceOutputOrgEnabled` in backend/domains/tts/service.ts). Mirror that here so // the toggle reflects effective state, not just persisted state. const parseConfig = createConfigParser(voiceOutputConfigSchema, () => ({ enabled: true, diff --git a/services/platform/app/features/settings/governance/data-subject-requests/file-request-dialog.tsx b/services/platform/app/features/settings/governance/data-subject-requests/file-request-dialog.tsx index dbc656dc99..6ad25f62d9 100644 --- a/services/platform/app/features/settings/governance/data-subject-requests/file-request-dialog.tsx +++ b/services/platform/app/features/settings/governance/data-subject-requests/file-request-dialog.tsx @@ -16,7 +16,7 @@ import { useToast } from '@/app/hooks/use-toast'; import { ERASURE_REASON_CODES, type ErasureReasonCode, -} from '@/convex/governance/erasure_constants'; +} from '@/backend/core/governance/erasure_constants'; import { useT } from '@/lib/i18n/client'; import { mapDsrError } from './data-subject-requests-errors'; diff --git a/services/platform/app/features/settings/governance/data-subject-requests/hooks/queries.ts b/services/platform/app/features/settings/governance/data-subject-requests/hooks/queries.ts index d3414cc63d..5aa366a4d3 100644 --- a/services/platform/app/features/settings/governance/data-subject-requests/hooks/queries.ts +++ b/services/platform/app/features/settings/governance/data-subject-requests/hooks/queries.ts @@ -1,6 +1,6 @@ import { useBackendQuery } from '@/app/hooks/use-backend-query'; import { useCachedPaginatedQuery } from '@/app/hooks/use-cached-paginated-query'; -import type { ErasureStatus } from '@/convex/governance/erasure_constants'; +import type { ErasureStatus } from '@/backend/core/governance/erasure_constants'; export function useListErasureRequests(args: { organizationId: string | undefined; diff --git a/services/platform/app/features/settings/governance/data-subject-requests/requests-list-section.tsx b/services/platform/app/features/settings/governance/data-subject-requests/requests-list-section.tsx index 4a40904fe2..d230ecb922 100644 --- a/services/platform/app/features/settings/governance/data-subject-requests/requests-list-section.tsx +++ b/services/platform/app/features/settings/governance/data-subject-requests/requests-list-section.tsx @@ -20,7 +20,7 @@ import { useAbility } from '@/app/hooks/use-ability'; import { ERASURE_STATUSES, type ErasureStatus, -} from '@/convex/governance/erasure_constants'; +} from '@/backend/core/governance/erasure_constants'; import { useT } from '@/lib/i18n/client'; import { FileRequestDialog } from './file-request-dialog'; diff --git a/services/platform/app/features/settings/governance/data-subject-requests/sla-countdown-badge.tsx b/services/platform/app/features/settings/governance/data-subject-requests/sla-countdown-badge.tsx index f25f236f6c..874c082d1a 100644 --- a/services/platform/app/features/settings/governance/data-subject-requests/sla-countdown-badge.tsx +++ b/services/platform/app/features/settings/governance/data-subject-requests/sla-countdown-badge.tsx @@ -7,7 +7,7 @@ import { type LucideIcon, } from 'lucide-react'; -import type { ErasureStatus } from '@/convex/governance/erasure_constants'; +import type { ErasureStatus } from '@/backend/core/governance/erasure_constants'; import { useT } from '@/lib/i18n/client'; import { cn } from '@/lib/utils/cn'; diff --git a/services/platform/app/features/settings/governance/data-subject-requests/status-badge.tsx b/services/platform/app/features/settings/governance/data-subject-requests/status-badge.tsx index 557fd15874..15b3b84aeb 100644 --- a/services/platform/app/features/settings/governance/data-subject-requests/status-badge.tsx +++ b/services/platform/app/features/settings/governance/data-subject-requests/status-badge.tsx @@ -12,7 +12,7 @@ import { type LucideIcon, } from 'lucide-react'; -import type { ErasureStatus } from '@/convex/governance/erasure_constants'; +import type { ErasureStatus } from '@/backend/core/governance/erasure_constants'; import { useT } from '@/lib/i18n/client'; import { cn } from '@/lib/utils/cn'; diff --git a/services/platform/app/features/settings/governance/hooks/queries.ts b/services/platform/app/features/settings/governance/hooks/queries.ts index a3f2de3bb0..f2ce29f17e 100644 --- a/services/platform/app/features/settings/governance/hooks/queries.ts +++ b/services/platform/app/features/settings/governance/hooks/queries.ts @@ -3,8 +3,8 @@ import { useMemo } from 'react'; import { useActionQuery } from '@/app/hooks/use-action-query'; import { useBackendQuery } from '@/app/hooks/use-backend-query'; import { useCachedPaginatedQuery } from '@/app/hooks/use-cached-paginated-query'; -import type { GOVERNANCE_POLICY_TYPES } from '@/convex/governance/schema'; -import type { SoftDeleteResourceType } from '@/convex/governance/soft_delete'; +import type { GOVERNANCE_POLICY_TYPES } from '@/backend/core/governance/schema'; +import type { SoftDeleteResourceType } from '@/backend/core/governance/soft_delete'; import { CHAT_MAX_FILE_SIZE, CHAT_UPLOAD_ALLOWED_TYPES, diff --git a/services/platform/app/features/shared/files/types.ts b/services/platform/app/features/shared/files/types.ts index 169748bb9a..ad4fcea0c8 100644 --- a/services/platform/app/features/shared/files/types.ts +++ b/services/platform/app/features/shared/files/types.ts @@ -1,4 +1,4 @@ -import type { FileAttachment } from './use-convex-file-upload'; +import type { FileAttachment } from './use-file-upload'; export type { FileAttachment }; diff --git a/services/platform/app/features/shared/files/use-convex-file-upload.cap.test.ts b/services/platform/app/features/shared/files/use-file-upload.cap.test.ts similarity index 93% rename from services/platform/app/features/shared/files/use-convex-file-upload.cap.test.ts rename to services/platform/app/features/shared/files/use-file-upload.cap.test.ts index 8807730273..c4c7fa0820 100644 --- a/services/platform/app/features/shared/files/use-convex-file-upload.cap.test.ts +++ b/services/platform/app/features/shared/files/use-file-upload.cap.test.ts @@ -7,7 +7,7 @@ import { toast } from '@/app/hooks/use-toast'; import { CHAT_MAX_FILE_COUNT, detectMediaMime } from '@/lib/shared/file-types'; import { compressImage } from '@/lib/utils/compress-image'; -import { useConvexFileUpload } from './use-convex-file-upload'; +import { useFileUpload } from './use-file-upload'; // --------------------------------------------------------------------------- // Regression coverage for the per-type size ceiling (#2048): audio/video files @@ -115,9 +115,9 @@ afterEach(() => { vi.unstubAllGlobals(); }); -describe('useConvexFileUpload — per-type size ceiling (#2048)', () => { +describe('useFileUpload — per-type size ceiling (#2048)', () => { it('accepts a 150MB audio file above the generic 100MB per-file cap', async () => { - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); await act(async () => { await result.current.uploadFiles([ @@ -142,7 +142,7 @@ describe('useConvexFileUpload — per-type size ceiling (#2048)', () => { allowedExtensions: [], }); - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); await act(async () => { await result.current.uploadFiles([ @@ -157,7 +157,7 @@ describe('useConvexFileUpload — per-type size ceiling (#2048)', () => { }); it('reports the elevated media ceiling (2048MB), not the 100MB generic cap, in the rejection toast', async () => { - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); await act(async () => { // 3 GB audio file — above the 2 GB media per-type cap, so it is rejected. @@ -186,7 +186,7 @@ describe('useConvexFileUpload — per-type size ceiling (#2048)', () => { allowedExtensions: [], }); - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); await act(async () => { await result.current.uploadFiles([ @@ -209,9 +209,9 @@ describe('useConvexFileUpload — per-type size ceiling (#2048)', () => { // 10-file cap. The hook now reserves slots for in-flight uploads so the second // batch sees the first's files and rejects the overflow. // --------------------------------------------------------------------------- -describe('useConvexFileUpload — in-flight slot cap (#2026)', () => { +describe('useFileUpload — in-flight slot cap (#2026)', () => { it('does not exceed the file cap when two batches overlap in-flight', async () => { - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); await act(async () => { const batchA = result.current.uploadFiles( @@ -238,7 +238,7 @@ describe('useConvexFileUpload — in-flight slot cap (#2026)', () => { }); it('rejects a duplicate dropped while the first copy is still uploading', async () => { - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); await act(async () => { const first = result.current.uploadFiles([ @@ -281,7 +281,7 @@ describe('useConvexFileUpload — in-flight slot cap (#2026)', () => { const flush = () => act(async () => new Promise((r) => setTimeout(r, 0))); - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); let batchA: Promise | undefined; let batchB: Promise | undefined; @@ -344,7 +344,7 @@ describe('useConvexFileUpload — in-flight slot cap (#2026)', () => { }), ); - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); await act(async () => { await result.current.uploadFiles([makeAudioFile('bad.mp3', 1 * MB)]); @@ -367,9 +367,9 @@ describe('useConvexFileUpload — in-flight slot cap (#2026)', () => { // trimmed batch was accepted) immediately followed by a blanket total-size // rejection — two contradictory toasts and zero uploads. // --------------------------------------------------------------------------- -describe('useConvexFileUpload — slot-overflow vs total-size ordering (#2029)', () => { +describe('useFileUpload — slot-overflow vs total-size ordering (#2029)', () => { it('shows only the total-size toast (not the slot-overflow toast) and uploads nothing when the trimmed batch still exceeds the total cap', async () => { - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); // Pre-fill 8 of the 10 slots with 21MB files → 168MB used, 2 slots left. await act(async () => { @@ -409,7 +409,7 @@ describe('useConvexFileUpload — slot-overflow vs total-size ordering (#2029)', }); it('still shows the slot-overflow toast when the trimmed batch fits under the total cap', async () => { - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); // Pre-fill 8 of the 10 slots with small 1MB files → 8MB used, 2 slots left. await act(async () => { @@ -451,7 +451,7 @@ describe('useConvexFileUpload — slot-overflow vs total-size ordering (#2029)', // (post-compression) size. `detectMediaMime` is forced to an image type and // `compressImage` is stubbed to shrink each image to ~1 MB. // --------------------------------------------------------------------------- -describe('useConvexFileUpload — total-size check uses compressed sizes (#2031)', () => { +describe('useFileUpload — total-size check uses compressed sizes (#2031)', () => { beforeEach(() => { detectMediaMimeMock.mockResolvedValue('image/png'); // Each image compresses down to 1 MB regardless of its raw size. @@ -472,7 +472,7 @@ describe('useConvexFileUpload — total-size check uses compressed sizes (#2031) }); it('accepts an image batch whose raw size exceeds the 200MB cap but compresses under it', async () => { - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); // 3 × 80MB = 240MB raw (> 200MB cap) but 3 × 1MB = 3MB compressed. await act(async () => { @@ -506,7 +506,7 @@ describe('useConvexFileUpload — total-size check uses compressed sizes (#2031) }; }); - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); await act(async () => { await result.current.uploadFiles([ diff --git a/services/platform/app/features/shared/files/use-convex-file-upload.indexing-toast.cap.test.ts b/services/platform/app/features/shared/files/use-file-upload.indexing-toast.cap.test.ts similarity index 93% rename from services/platform/app/features/shared/files/use-convex-file-upload.indexing-toast.cap.test.ts rename to services/platform/app/features/shared/files/use-file-upload.indexing-toast.cap.test.ts index a95551b89b..5ab251e451 100644 --- a/services/platform/app/features/shared/files/use-convex-file-upload.indexing-toast.cap.test.ts +++ b/services/platform/app/features/shared/files/use-file-upload.indexing-toast.cap.test.ts @@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { useUploadPolicy } from '@/app/features/settings/governance/hooks/queries'; import { toast } from '@/app/hooks/use-toast'; -import { useConvexFileUpload } from './use-convex-file-upload'; +import { useFileUpload } from './use-file-upload'; // --------------------------------------------------------------------------- // Regression coverage for #1457: a PDF (or any RAG-indexable file) must NOT @@ -98,10 +98,10 @@ afterEach(() => { vi.unstubAllGlobals(); }); -describe('useConvexFileUpload — deferred indexing toast (#1457)', () => { +describe('useFileUpload — deferred indexing toast (#1457)', () => { it('does NOT show "uploaded successfully" immediately for an indexable PDF', async () => { const { result } = renderHook(() => - useConvexFileUpload({ organizationId: 'org-1' }), + useFileUpload({ organizationId: 'org-1' }), ); await act(async () => { @@ -119,7 +119,7 @@ describe('useConvexFileUpload — deferred indexing toast (#1457)', () => { it('shows "uploaded successfully" immediately when indexing is disabled', async () => { const { result } = renderHook(() => - useConvexFileUpload({ organizationId: 'org-1', disableIndexing: true }), + useFileUpload({ organizationId: 'org-1', disableIndexing: true }), ); await act(async () => { diff --git a/services/platform/app/features/shared/files/use-convex-file-upload.test.ts b/services/platform/app/features/shared/files/use-file-upload.test.ts similarity index 96% rename from services/platform/app/features/shared/files/use-convex-file-upload.test.ts rename to services/platform/app/features/shared/files/use-file-upload.test.ts index 4a8298dba1..128d779fec 100644 --- a/services/platform/app/features/shared/files/use-convex-file-upload.test.ts +++ b/services/platform/app/features/shared/files/use-file-upload.test.ts @@ -9,7 +9,7 @@ import { } from '@/lib/shared/file-types'; import { compressImage } from '@/lib/utils/compress-image'; -import { useConvexFileUpload } from './use-convex-file-upload'; +import { useFileUpload } from './use-file-upload'; // --------------------------------------------------------------------------- // Module mocks. The hook leans on Convex mutations, the upload policy query, @@ -163,9 +163,9 @@ afterEach(() => { vi.unstubAllGlobals(); }); -describe('useConvexFileUpload — concurrent-batch cap & dedup', () => { +describe('useFileUpload — concurrent-batch cap & dedup', () => { it('counts in-flight uploads against the 10-file cap across batches', async () => { - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); // Batch A: 6 files, held in-flight (fetch parked). const batchA = Array.from({ length: 6 }, (_, i) => @@ -202,7 +202,7 @@ describe('useConvexFileUpload — concurrent-batch cap & dedup', () => { }); it('dedupes a file already uploading in another batch', async () => { - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); const fileA = makeFile('dup.txt', 42, 'text/plain'); let promiseA!: Promise; @@ -232,7 +232,7 @@ describe('useConvexFileUpload — concurrent-batch cap & dedup', () => { }); it('dedupes a re-attached image against its original (pre-compression) identity', async () => { - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); // Upload an image; compression renames .png -> .jpg and shrinks the size. const image = makeFile('photo.png', 1_500_000, 'image/png'); @@ -268,7 +268,7 @@ describe('useConvexFileUpload — concurrent-batch cap & dedup', () => { }); it('counts in-flight uploads against the total-size cap across batches', async () => { - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); const MB = 1024 * 1024; // Batch A: 180 MB held in-flight (2 files, each under the per-file cap). @@ -312,7 +312,7 @@ describe('useConvexFileUpload — concurrent-batch cap & dedup', () => { // indexing finishes, #1457) — the test uses that toast as its timing hook // into the commit→render gap below. const { result } = renderHook(() => - useConvexFileUpload({ ...config, disableIndexing: true }), + useFileUpload({ ...config, disableIndexing: true }), ); const CAP = CHAT_MAX_FILE_COUNT; @@ -370,7 +370,7 @@ describe('useConvexFileUpload — concurrent-batch cap & dedup', () => { }); it('frees an in-flight reservation when its upload fails', async () => { - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); // One file in-flight, holding a reservation. const failing = makeFile('fail.txt', 10, 'text/plain'); @@ -417,7 +417,7 @@ describe('useConvexFileUpload — concurrent-batch cap & dedup', () => { }); it('cancels an in-flight upload without committing it or toasting a failure', async () => { - const { result } = renderHook(() => useConvexFileUpload(config)); + const { result } = renderHook(() => useFileUpload(config)); // One file parked in-flight — a single upload spinner, nothing committed. const file = makeFile('cancel-me.txt', 10, 'text/plain'); diff --git a/services/platform/app/features/shared/files/use-convex-file-upload.ts b/services/platform/app/features/shared/files/use-file-upload.ts similarity index 99% rename from services/platform/app/features/shared/files/use-convex-file-upload.ts rename to services/platform/app/features/shared/files/use-file-upload.ts index 56c3198c53..694aad5bc7 100644 --- a/services/platform/app/features/shared/files/use-convex-file-upload.ts +++ b/services/platform/app/features/shared/files/use-file-upload.ts @@ -47,7 +47,7 @@ interface FileAttachment { originalFileSize?: number; } -interface ConvexFileUploadConfig { +interface FileUploadConfig { organizationId: string; /** * The chat thread the upload belongs to. When provided, the @@ -80,7 +80,7 @@ const DEFAULT_UPLOAD_CONFIG = { allowedTypes: [...CHAT_UPLOAD_ALLOWED_TYPES], }; -export function useConvexFileUpload(config: ConvexFileUploadConfig) { +export function useFileUpload(config: FileUploadConfig) { const { t } = useT('chat'); const [attachments, setAttachments] = useState([]); const [uploadingFiles, setUploadingFiles] = useState([]); @@ -677,4 +677,4 @@ export function useConvexFileUpload(config: ConvexFileUploadConfig) { }; } -export type { FileAttachment, ConvexFileUploadConfig }; +export type { FileAttachment, FileUploadConfig }; diff --git a/services/platform/app/features/shared/mentions/use-kb-mentions.ts b/services/platform/app/features/shared/mentions/use-kb-mentions.ts index 169d32813e..8983a39f8a 100644 --- a/services/platform/app/features/shared/mentions/use-kb-mentions.ts +++ b/services/platform/app/features/shared/mentions/use-kb-mentions.ts @@ -1,8 +1,8 @@ import { useCallback, useRef, useState } from 'react'; -import type { BlobRef } from '@/convex/lib/storage/blob_ref'; +import type { BlobRef } from '@/backend/core/lib/storage/blob_ref'; -/** Mirrors `MAX_KB_REFERENCES` in convex/agents/chat_turn.ts. */ +/** The cap on knowledge references a chat turn accepts. */ export const MAX_KB_MENTIONS = 5; /** @@ -87,7 +87,7 @@ export function useKbMentions(): UseKbMentionsResult { // Synchronous source of truth alongside the render state: updated at // mutation time (not render time) so clearMentions can return the snapshot // and back-to-back adds in one tick don't read a stale list. Same pattern - // as use-convex-file-upload's attachmentsRef. + // as use-file-upload's attachmentsRef. const mentionsRef = useRef(mentions); const commit = useCallback((next: KbMention[]) => { mentionsRef.current = next; diff --git a/services/platform/app/features/tasks/components/mention-trigger-chips.tsx b/services/platform/app/features/tasks/components/mention-trigger-chips.tsx index 3915083318..c7ea685d08 100644 --- a/services/platform/app/features/tasks/components/mention-trigger-chips.tsx +++ b/services/platform/app/features/tasks/components/mention-trigger-chips.tsx @@ -3,7 +3,7 @@ import { Ban, Zap } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; -import { parseMentionTokens } from '@/convex/tasks/mentions'; +import { parseMentionTokens } from '@/backend/core/tasks/mentions'; import { useT } from '@/lib/i18n/client'; import { cn } from '@/lib/utils/cn'; diff --git a/services/platform/app/features/tasks/components/reviewer-picker.tsx b/services/platform/app/features/tasks/components/reviewer-picker.tsx index 442de455f5..953c5ef2dc 100644 --- a/services/platform/app/features/tasks/components/reviewer-picker.tsx +++ b/services/platform/app/features/tasks/components/reviewer-picker.tsx @@ -13,7 +13,7 @@ import { type SearchableSelectOption, } from '@/app/components/ui/forms/searchable-select'; import { Tooltip } from '@/app/components/ui/overlays/tooltip'; -import { EDITOR_ROLES } from '@/convex/projects/access'; +import { EDITOR_ROLES } from '@/backend/core/projects/access'; import { useT } from '@/lib/i18n/client'; import { useAssignableActors } from '../hooks/use-actor-directory'; diff --git a/services/platform/app/features/tasks/components/task-attachments.tsx b/services/platform/app/features/tasks/components/task-attachments.tsx index f48c2369b4..34b78efe98 100644 --- a/services/platform/app/features/tasks/components/task-attachments.tsx +++ b/services/platform/app/features/tasks/components/task-attachments.tsx @@ -7,7 +7,7 @@ import { useId, useMemo, useState } from 'react'; import { FileUpload } from '@/app/components/ui/forms/file-upload'; import { FileAttachmentDisplay } from '@/app/features/shared/files/file-displays'; -import type { FileAttachment } from '@/app/features/shared/files/use-convex-file-upload'; +import type { FileAttachment } from '@/app/features/shared/files/use-file-upload'; import { useFileUrls } from '@/app/features/shared/files/use-file-url'; import { ImagePreviewDialog, @@ -20,7 +20,7 @@ import { cn } from '@/lib/utils/cn'; /** * Task image/document attachments — shared by the create draft and the saved * detail view. Purely presentational: the parent owns the list and wires upload - * + remove (create accumulates in `useConvexFileUpload`; edit persists each + * + remove (create accumulates in `useFileUpload`; edit persists each * change through `updateTask`). Reuses the chat {@link FileAttachmentDisplay} * renderer (image thumbnail / file chip) and the {@link FileUpload} drop-zone * primitive. Read-only callers with no attachments render nothing. diff --git a/services/platform/app/features/tasks/components/task-modal.tsx b/services/platform/app/features/tasks/components/task-modal.tsx index 74aafffc30..4c348f795b 100644 --- a/services/platform/app/features/tasks/components/task-modal.tsx +++ b/services/platform/app/features/tasks/components/task-modal.tsx @@ -38,14 +38,14 @@ import { useProject } from '@/app/features/projects/hooks/queries'; import { extractPastedImageFiles } from '@/app/features/shared/files/clipboard-images'; import { type FileAttachment, - useConvexFileUpload, -} from '@/app/features/shared/files/use-convex-file-upload'; + useFileUpload, +} from '@/app/features/shared/files/use-file-upload'; import { useBackendAction } from '@/app/hooks/use-backend-action'; import { useBackendQuery } from '@/app/hooks/use-backend-query'; import { useCurrentMemberContext } from '@/app/hooks/use-current-member-context'; import { useFormatDate } from '@/app/hooks/use-format-date'; import { toast } from '@/app/hooks/use-toast'; -import { TASK_TITLE_MAX } from '@/convex/tasks/helpers'; +import { TASK_TITLE_MAX } from '@/backend/core/tasks/helpers'; import { useT } from '@/lib/i18n/client'; import { AppError } from '@/lib/shared/errors/app-error'; import { TASK_UPLOAD_ALLOWED_TYPES } from '@/lib/shared/file-types'; @@ -714,7 +714,7 @@ function CreateTaskBody({ const activeTemplate = templates.find((entry) => entry.automationSlug === templateSlug) ?? null; const { attachments, uploadingFiles, uploadFiles, removeAttachment } = - useConvexFileUpload({ + useFileUpload({ organizationId, allowedTypes: [...TASK_UPLOAD_ALLOWED_TYPES], }); @@ -1020,12 +1020,10 @@ function EditTaskBody({ const assignTask = useAssignTask(); const setTaskReviewer = useSetTaskReviewer(); const createTask = useCreateTask(); - const { uploadingFiles, uploadFiles, clearAttachments } = useConvexFileUpload( - { - organizationId: task?.organizationId ?? '', - allowedTypes: [...TASK_UPLOAD_ALLOWED_TYPES], - }, - ); + const { uploadingFiles, uploadFiles, clearAttachments } = useFileUpload({ + organizationId: task?.organizationId ?? '', + allowedTypes: [...TASK_UPLOAD_ALLOWED_TYPES], + }); const [subtaskTitle, setSubtaskTitle] = useState(''); const [archiveOpen, setArchiveOpen] = useState(false); diff --git a/services/platform/app/features/tasks/lib/display.ts b/services/platform/app/features/tasks/lib/display.ts index 825b46152f..b16e6c9a55 100644 --- a/services/platform/app/features/tasks/lib/display.ts +++ b/services/platform/app/features/tasks/lib/display.ts @@ -7,7 +7,7 @@ import type { TaskRow } from '@/app/lib/backend/contract/docs'; /** * One attached label as the read paths return it. The stored document holds - * `labelIds` into the project catalog; `convex/tasks/queries.ts` resolves + * `labelIds` into the project catalog; the task read layer resolves * those to `{ id, name, color }` before they reach the client. `id` is absent * only for a document still carrying pre-catalog string labels (see * `withResolvedLabels`' mid-migration fallback). @@ -63,7 +63,7 @@ export function isTaskStatus(value: string): value is TaskStatus { } /** - * Maps a stored activity `action` (see convex/tasks/helpers.ts `recordActivity`) + * Maps a stored activity `action` (see backend/core/tasks/helpers.ts `recordActivity`) * to its `tasks` i18n key. Unknown actions fall back to the raw string at the * call site, so the timeline degrades gracefully if a new action ships. */ @@ -84,7 +84,7 @@ export const TASK_ACTIVITY_LABEL_KEY: Record = { /** * Maps a run-admission `refusedReason` code (stored as the `toValue` of an - * `'agent_run.refused'` activity row — see convex/agents/run_agent_on_task.ts) + * `'agent_run.refused'` activity row) * to its `tasks` i18n key. Unknown codes fall back to the raw string at the * call site. Lowercase phrases: they render mid-sentence in the timeline. */ diff --git a/services/platform/app/features/tasks/lib/mention-actor-options.ts b/services/platform/app/features/tasks/lib/mention-actor-options.ts index 1cca7c75c8..bf1a9c507e 100644 --- a/services/platform/app/features/tasks/lib/mention-actor-options.ts +++ b/services/platform/app/features/tasks/lib/mention-actor-options.ts @@ -20,14 +20,14 @@ export interface MentionActorOption { name: string; email?: string; /** The `@token` inserted into the text — picked to match a handle the - * server directory resolves (`convex/tasks/directory.ts::memberHandles`). */ + * server directory resolves (`backend/domains/collab/mention-directory.ts::memberHandles`). */ handle: string; } /** * Mentionable actors for a project, in picker order: org members first, then * agents, then the automations operating this board — the same population the - * server resolves mentions against (`convex/tasks/directory.ts`). Agent + * server resolves mentions against (`backend/domains/collab/mention-directory.ts`). Agent * scoping follows the project agent gates: the default `agentMode: 'all'` * exposes every org agent (recommended ones first); `'restricted'` limits the * list to the project's `allowedAgentSlugs`. Automations are the deployed diff --git a/services/platform/app/features/tasks/lib/mention-handles.ts b/services/platform/app/features/tasks/lib/mention-handles.ts index 06a00dc291..83e3fb6fd9 100644 --- a/services/platform/app/features/tasks/lib/mention-handles.ts +++ b/services/platform/app/features/tasks/lib/mention-handles.ts @@ -1,13 +1,13 @@ /** * Client-side mirror of the server's mention-handle derivation - * (`convex/tasks/directory.ts::memberHandles`). The composer inserts the + * (`backend/domains/collab/mention-directory.ts::memberHandles`). The composer inserts the * FIRST server-resolvable handle; the read views resolve EVERY variant back * to a display name, so a mention typed in any form (`@alice.smith`, * `@alicesmith`, `@alice`) renders as the person's name. */ /** The plain-text token charset the server's mention parser accepts - * (`convex/tasks/mentions.ts::MENTION_RE`) — a handle outside it can never + * (`backend/core/tasks/mentions.ts::MENTION_RE`) — a handle outside it can never * resolve, so such candidates are skipped. Includes `/` so pack agent * slugs (`github/create-pull-requests/pr-creator`) round-trip. */ export const MENTION_TOKEN_RE = /^[a-zA-Z0-9._/-]+$/; @@ -47,7 +47,7 @@ export interface MentionableAgent { } /** All candidate handles for a project agent instance, lowercased, the - * server derivation order (`convex/tasks/directory.ts::agentInstanceHandles`): + * server derivation order (`backend/domains/collab/mention-directory.ts::agentInstanceHandles`): * dotted name → squashed name → instance id (the collision-proof fallback * the server also resolves). */ export function agentHandleVariants(agent: MentionableAgent): string[] { @@ -81,7 +81,7 @@ export interface MentionableAutomation { } /** All candidate handles for a deployed automation, lowercased, the server - * derivation order (`convex/tasks/directory.ts::automationHandles`): store + * derivation order (`backend/domains/collab/mention-directory.ts::automationHandles`): store * name → dotted display name → squashed display name. */ export function automationHandleVariants( automation: MentionableAutomation, diff --git a/services/platform/app/features/websites/components/website-pages-dialog.tsx b/services/platform/app/features/websites/components/website-pages-dialog.tsx index 07414e6388..c32816f8b7 100644 --- a/services/platform/app/features/websites/components/website-pages-dialog.tsx +++ b/services/platform/app/features/websites/components/website-pages-dialog.tsx @@ -28,7 +28,7 @@ import type { CrawlerChunk, CrawlerPage, CrawlerSearchResult, -} from '@/convex/websites/types'; +} from '@/backend/core/websites/types'; import { useT } from '@/lib/i18n/client'; const PAGE_SIZE = 20; diff --git a/services/platform/app/features/websites/components/website-view-dialog.tsx b/services/platform/app/features/websites/components/website-view-dialog.tsx index 56c42acda6..6dd2a76adf 100644 --- a/services/platform/app/features/websites/components/website-view-dialog.tsx +++ b/services/platform/app/features/websites/components/website-view-dialog.tsx @@ -33,7 +33,7 @@ import type { CrawlerChunk, CrawlerPage, CrawlerSearchResult, -} from '@/convex/websites/types'; +} from '@/backend/core/websites/types'; import { useT } from '@/lib/i18n/client'; import { isScanPaused } from '../lib/scan-paused'; diff --git a/services/platform/app/features/websites/lib/scan-paused.ts b/services/platform/app/features/websites/lib/scan-paused.ts index 5e8b03ae4b..d43e3fb74e 100644 --- a/services/platform/app/features/websites/lib/scan-paused.ts +++ b/services/platform/app/features/websites/lib/scan-paused.ts @@ -3,7 +3,7 @@ import type { WebsiteDoc } from '@/app/lib/backend/contract/docs'; /** * Whether this website's scans are paused — the `metadata.scanPausedAt` flag * the crawler writes after repeated failures to reach the organization's - * knowledge database (see convex/websites/scan_scheduling.ts). Paused rows + * knowledge database (see backend/core/websites/scan_scheduling.ts). Paused rows * keep `status: 'error'`; this flag is what distinguishes "failed, will * retry" from "gave up, needs a manual resume". */ diff --git a/services/platform/app/lib/loader-preload.ts b/services/platform/app/lib/loader-preload.ts index dbec29bfc6..5cdd269f4a 100644 --- a/services/platform/app/lib/loader-preload.ts +++ b/services/platform/app/lib/loader-preload.ts @@ -5,7 +5,7 @@ import { import type { ArgsOf, QueryName } from '@/app/lib/backend/contract'; import { MissingBackendRowError } from '@/app/lib/backend/missing-row'; import type { RouterContext } from '@/app/router'; -import type { GOVERNANCE_POLICY_TYPES } from '@/convex/governance/schema'; +import type { GOVERNANCE_POLICY_TYPES } from '@/backend/core/governance/schema'; import { AppError } from '@/lib/shared/errors/app-error'; type QueryArgs = diff --git a/services/platform/backend/auth/auth.ts b/services/platform/backend/auth/auth.ts index b4c718e67c..dcc6912d2f 100644 --- a/services/platform/backend/auth/auth.ts +++ b/services/platform/backend/auth/auth.ts @@ -7,13 +7,13 @@ import { organization, twoFactor } from 'better-auth/plugins'; import pg from 'pg'; import type { Sql } from 'postgres'; -import { getClientIp } from '../../convex/lib/utils/client_ip.ts'; import { assertValidOrgSlug } from '../../lib/shared/constants/org-slug.ts'; import { isReservedOrgSlug } from '../../lib/shared/constants/reserved-org-slugs.ts'; import { DEFAULT_TRUSTED_PROXIES } from '../../lib/shared/schemas/governance.ts'; import { organizationNameSchema } from '../../lib/shared/schemas/organizations.ts'; import { sessionIdleWindowSeconds } from '../../lib/shared/session-idle.ts'; import { getString, isRecord } from '../../lib/utils/type-utils.ts'; +import { getClientIp } from '../core/lib/utils/client_ip.ts'; import { logJoinedOrganization } from '../domains/audit_logs/service.ts'; import { clearOnSuccess, diff --git a/services/platform/convex/README.md b/services/platform/backend/core/README.md similarity index 100% rename from services/platform/convex/README.md rename to services/platform/backend/core/README.md diff --git a/services/platform/convex/accounts/microsoft_account.test.ts b/services/platform/backend/core/accounts/microsoft_account.test.ts similarity index 100% rename from services/platform/convex/accounts/microsoft_account.test.ts rename to services/platform/backend/core/accounts/microsoft_account.test.ts diff --git a/services/platform/convex/accounts/microsoft_account.ts b/services/platform/backend/core/accounts/microsoft_account.ts similarity index 100% rename from services/platform/convex/accounts/microsoft_account.ts rename to services/platform/backend/core/accounts/microsoft_account.ts diff --git a/services/platform/convex/agent_secrets/constants.test.ts b/services/platform/backend/core/agent_secrets/constants.test.ts similarity index 100% rename from services/platform/convex/agent_secrets/constants.test.ts rename to services/platform/backend/core/agent_secrets/constants.test.ts diff --git a/services/platform/convex/agent_secrets/constants.ts b/services/platform/backend/core/agent_secrets/constants.ts similarity index 100% rename from services/platform/convex/agent_secrets/constants.ts rename to services/platform/backend/core/agent_secrets/constants.ts diff --git a/services/platform/convex/agents/file_actions.test.ts b/services/platform/backend/core/agents/file_actions.test.ts similarity index 99% rename from services/platform/convex/agents/file_actions.test.ts rename to services/platform/backend/core/agents/file_actions.test.ts index 8c07598822..0ac7e0456b 100644 --- a/services/platform/convex/agents/file_actions.test.ts +++ b/services/platform/backend/core/agents/file_actions.test.ts @@ -6,7 +6,7 @@ import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; // The `*ForCaller` functions ARE the agent-file surface now — the Convex // action wrappers that used to delegate to them retired with the runtime — diff --git a/services/platform/convex/agents/file_actions.ts b/services/platform/backend/core/agents/file_actions.ts similarity index 98% rename from services/platform/convex/agents/file_actions.ts rename to services/platform/backend/core/agents/file_actions.ts index 323ddbcab5..8f64180836 100644 --- a/services/platform/convex/agents/file_actions.ts +++ b/services/platform/backend/core/agents/file_actions.ts @@ -7,18 +7,18 @@ import { readOrgAgent, type AgentViewer, type OrgAgent, -} from '../../lib/agents/listing'; +} from '../../../lib/agents/listing'; import { AgentParseError, parseAgentYaml, serializeAgentYaml, -} from '../../lib/agents/parse'; -import { resolveAgentForTurn } from '../../lib/agents/resolve'; -import { AppError } from '../../lib/shared/errors/app-error'; +} from '../../../lib/agents/parse'; +import { resolveAgentForTurn } from '../../../lib/agents/resolve'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { isValidAgentSlug, type AgentDefinition, -} from '../../lib/shared/schemas/agents'; +} from '../../../lib/shared/schemas/agents'; import { createOrgAgentReader, listAgentHistoryEntries, diff --git a/services/platform/convex/agents/file_utils.test.ts b/services/platform/backend/core/agents/file_utils.test.ts similarity index 98% rename from services/platform/convex/agents/file_utils.test.ts rename to services/platform/backend/core/agents/file_utils.test.ts index 79ac13d4ba..f4a9a112cd 100644 --- a/services/platform/convex/agents/file_utils.test.ts +++ b/services/platform/backend/core/agents/file_utils.test.ts @@ -14,7 +14,7 @@ import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { readOrgAgent, readOrgAgents } from '../../lib/agents/listing'; +import { readOrgAgent, readOrgAgents } from '../../../lib/agents/listing'; import { createOrgAgentReader, listAgentSlugs, diff --git a/services/platform/convex/agents/file_utils.ts b/services/platform/backend/core/agents/file_utils.ts similarity index 98% rename from services/platform/convex/agents/file_utils.ts rename to services/platform/backend/core/agents/file_utils.ts index 472f32bd68..0dd08913c3 100644 --- a/services/platform/convex/agents/file_utils.ts +++ b/services/platform/backend/core/agents/file_utils.ts @@ -25,11 +25,11 @@ import path from 'node:path'; -import type { AgentFileReader } from '../../lib/agents/listing'; +import type { AgentFileReader } from '../../../lib/agents/listing'; import { isValidAgentSlug, MAX_AGENT_FILE_BYTES, -} from '../../lib/shared/schemas/agents'; +} from '../../../lib/shared/schemas/agents'; import { atomicWrite, generateHistoryTimestamp, diff --git a/services/platform/convex/agents/views.ts b/services/platform/backend/core/agents/views.ts similarity index 98% rename from services/platform/convex/agents/views.ts rename to services/platform/backend/core/agents/views.ts index 3836969cd0..69123c451a 100644 --- a/services/platform/convex/agents/views.ts +++ b/services/platform/backend/core/agents/views.ts @@ -11,7 +11,7 @@ import type { AgentKnowledgeScope, AgentVisibility, -} from '../../lib/shared/schemas/agents'; +} from '../../../lib/shared/schemas/agents'; /** The fields every agent view carries. */ export interface AgentSummaryView { diff --git a/services/platform/convex/approvals/policy.test.ts b/services/platform/backend/core/approvals/policy.test.ts similarity index 97% rename from services/platform/convex/approvals/policy.test.ts rename to services/platform/backend/core/approvals/policy.test.ts index 545a6b356d..cc6137d05c 100644 --- a/services/platform/convex/approvals/policy.test.ts +++ b/services/platform/backend/core/approvals/policy.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest'; -import { approvalPolicyConfigSchema } from '../../lib/shared/schemas/governance'; +import { approvalPolicyConfigSchema } from '../../../lib/shared/schemas/governance'; import { resolveApprovalRequirement } from './policy'; const platformWrite = { diff --git a/services/platform/convex/approvals/policy.ts b/services/platform/backend/core/approvals/policy.ts similarity index 97% rename from services/platform/convex/approvals/policy.ts rename to services/platform/backend/core/approvals/policy.ts index e122610309..f4d469356b 100644 --- a/services/platform/convex/approvals/policy.ts +++ b/services/platform/backend/core/approvals/policy.ts @@ -17,7 +17,7 @@ * `connector`, and either beats the built-in default. */ -import type { ApprovalPolicyConfig } from '../../lib/shared/schemas/governance'; +import type { ApprovalPolicyConfig } from '../../../lib/shared/schemas/governance'; export type ApprovalRequirement = 'allow' | 'require'; diff --git a/services/platform/convex/approvals/types.ts b/services/platform/backend/core/approvals/types.ts similarity index 100% rename from services/platform/convex/approvals/types.ts rename to services/platform/backend/core/approvals/types.ts diff --git a/services/platform/convex/audit_logs/agent_run_ledger.ts b/services/platform/backend/core/audit_logs/agent_run_ledger.ts similarity index 99% rename from services/platform/convex/audit_logs/agent_run_ledger.ts rename to services/platform/backend/core/audit_logs/agent_run_ledger.ts index 9cfd3d9074..f4801565bf 100644 --- a/services/platform/convex/audit_logs/agent_run_ledger.ts +++ b/services/platform/backend/core/audit_logs/agent_run_ledger.ts @@ -18,7 +18,7 @@ * this module performs is index-backed with an explicit scan bound. */ -import { isRecord } from '../../lib/utils/type-utils'; +import { isRecord } from '../../../lib/utils/type-utils'; import type { MutationCtx } from '../lib/ctx'; import type { Doc } from '../lib/rows'; import { convexStorageId } from '../lib/storage/blob_ref'; diff --git a/services/platform/convex/audit_logs/emit.ts b/services/platform/backend/core/audit_logs/emit.ts similarity index 100% rename from services/platform/convex/audit_logs/emit.ts rename to services/platform/backend/core/audit_logs/emit.ts diff --git a/services/platform/convex/audit_logs/helpers.test.ts b/services/platform/backend/core/audit_logs/helpers.test.ts similarity index 100% rename from services/platform/convex/audit_logs/helpers.test.ts rename to services/platform/backend/core/audit_logs/helpers.test.ts diff --git a/services/platform/convex/audit_logs/helpers.ts b/services/platform/backend/core/audit_logs/helpers.ts similarity index 99% rename from services/platform/convex/audit_logs/helpers.ts rename to services/platform/backend/core/audit_logs/helpers.ts index 2a12a20ee4..778aeb5a77 100644 --- a/services/platform/convex/audit_logs/helpers.ts +++ b/services/platform/backend/core/audit_logs/helpers.ts @@ -1,4 +1,4 @@ -import { isRecord } from '../../lib/utils/type-utils'; +import { isRecord } from '../../../lib/utils/type-utils'; import type { MutationCtx, QueryCtx } from '../lib/ctx'; import { computeAuditHash, diff --git a/services/platform/convex/audit_logs/types.ts b/services/platform/backend/core/audit_logs/types.ts similarity index 100% rename from services/platform/convex/audit_logs/types.ts rename to services/platform/backend/core/audit_logs/types.ts diff --git a/services/platform/convex/automations/agent_host.ts b/services/platform/backend/core/automations/agent_host.ts similarity index 99% rename from services/platform/convex/automations/agent_host.ts rename to services/platform/backend/core/automations/agent_host.ts index b2b56cd128..cbce924801 100644 --- a/services/platform/convex/automations/agent_host.ts +++ b/services/platform/backend/core/automations/agent_host.ts @@ -25,7 +25,7 @@ import { randomBytes, randomUUID } from 'node:crypto'; -import type { SkillViewer } from '../../lib/skills/visibility'; +import type { SkillViewer } from '../../../lib/skills/visibility'; import { buildExternalTurnExec, classifyHarnessEnd, diff --git a/services/platform/convex/automations/agent_retry.ts b/services/platform/backend/core/automations/agent_retry.ts similarity index 100% rename from services/platform/convex/automations/agent_retry.ts rename to services/platform/backend/core/automations/agent_retry.ts diff --git a/services/platform/convex/automations/ask_answer_carryover.test.ts b/services/platform/backend/core/automations/ask_answer_carryover.test.ts similarity index 100% rename from services/platform/convex/automations/ask_answer_carryover.test.ts rename to services/platform/backend/core/automations/ask_answer_carryover.test.ts diff --git a/services/platform/convex/automations/ask_answer_carryover.ts b/services/platform/backend/core/automations/ask_answer_carryover.ts similarity index 100% rename from services/platform/convex/automations/ask_answer_carryover.ts rename to services/platform/backend/core/automations/ask_answer_carryover.ts diff --git a/services/platform/convex/automations/bound_run_payload.test.ts b/services/platform/backend/core/automations/bound_run_payload.test.ts similarity index 98% rename from services/platform/convex/automations/bound_run_payload.test.ts rename to services/platform/backend/core/automations/bound_run_payload.test.ts index e43a573b98..98b672ae1e 100644 --- a/services/platform/convex/automations/bound_run_payload.test.ts +++ b/services/platform/backend/core/automations/bound_run_payload.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { NodeTrace } from '../../lib/engine/core/types'; +import type { NodeTrace } from '../../../lib/engine/core/types'; import { boundCheckpointTrace, boundNodeTrace, diff --git a/services/platform/convex/automations/bound_run_payload.ts b/services/platform/backend/core/automations/bound_run_payload.ts similarity index 97% rename from services/platform/convex/automations/bound_run_payload.ts rename to services/platform/backend/core/automations/bound_run_payload.ts index b51cc597ef..377794a3ef 100644 --- a/services/platform/convex/automations/bound_run_payload.ts +++ b/services/platform/backend/core/automations/bound_run_payload.ts @@ -38,8 +38,8 @@ * keeps a default install bounded. */ -import type { NodeTrace } from '../../lib/engine/core/types'; -import { boundJson } from '../../lib/shared/utils/bound-json'; +import type { NodeTrace } from '../../../lib/engine/core/types'; +import { boundJson } from '../../../lib/shared/utils/bound-json'; import type { NodeCheckpoint } from './checkpoints'; /** diff --git a/services/platform/convex/automations/checkpoints.ts b/services/platform/backend/core/automations/checkpoints.ts similarity index 99% rename from services/platform/convex/automations/checkpoints.ts rename to services/platform/backend/core/automations/checkpoints.ts index ecc9f92c27..31451672d3 100644 --- a/services/platform/convex/automations/checkpoints.ts +++ b/services/platform/backend/core/automations/checkpoints.ts @@ -17,7 +17,7 @@ * Kept free of Convex imports so it can be exercised directly. */ -import type { Effect, NodeTrace } from '../../lib/engine/core/types'; +import type { Effect, NodeTrace } from '../../../lib/engine/core/types'; /** Why a node produced no output. */ export type SkipReason = diff --git a/services/platform/convex/automations/cron.ts b/services/platform/backend/core/automations/cron.ts similarity index 100% rename from services/platform/convex/automations/cron.ts rename to services/platform/backend/core/automations/cron.ts diff --git a/services/platform/convex/automations/liveness.ts b/services/platform/backend/core/automations/liveness.ts similarity index 100% rename from services/platform/convex/automations/liveness.ts rename to services/platform/backend/core/automations/liveness.ts diff --git a/services/platform/convex/automations/llm_call.test.ts b/services/platform/backend/core/automations/llm_call.test.ts similarity index 100% rename from services/platform/convex/automations/llm_call.test.ts rename to services/platform/backend/core/automations/llm_call.test.ts diff --git a/services/platform/convex/automations/llm_call.ts b/services/platform/backend/core/automations/llm_call.ts similarity index 99% rename from services/platform/convex/automations/llm_call.ts rename to services/platform/backend/core/automations/llm_call.ts index efbf1c44e6..06af52dde2 100644 --- a/services/platform/convex/automations/llm_call.ts +++ b/services/platform/backend/core/automations/llm_call.ts @@ -26,7 +26,7 @@ import { Ajv } from 'ajv'; import type { BuilderMessage, BuilderModel, -} from '../../lib/automations_builder/session'; +} from '../../../lib/automations_builder/session'; import { createBuilderModel, type BuilderModelTarget, diff --git a/services/platform/convex/automations/pack_zip.test.ts b/services/platform/backend/core/automations/pack_zip.test.ts similarity index 99% rename from services/platform/convex/automations/pack_zip.test.ts rename to services/platform/backend/core/automations/pack_zip.test.ts index c1c166131e..70146e8e3a 100644 --- a/services/platform/convex/automations/pack_zip.test.ts +++ b/services/platform/backend/core/automations/pack_zip.test.ts @@ -3,7 +3,7 @@ import JSZip from 'jszip'; import { describe, expect, it } from 'vitest'; -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { collectSkillReferences, parseAutomationPackZip } from './pack_zip'; const WORKFLOW = 'name: demo\nnodes: []\n'; diff --git a/services/platform/convex/automations/pack_zip.ts b/services/platform/backend/core/automations/pack_zip.ts similarity index 97% rename from services/platform/convex/automations/pack_zip.ts rename to services/platform/backend/core/automations/pack_zip.ts index b3ba1609d5..d47316bcaf 100644 --- a/services/platform/convex/automations/pack_zip.ts +++ b/services/platform/backend/core/automations/pack_zip.ts @@ -30,19 +30,19 @@ import JSZip from 'jszip'; -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { MAX_AUTOMATION_BUNDLE_ENTRIES, MAX_AUTOMATION_BUNDLE_FILE_BYTES, MAX_AUTOMATION_BUNDLE_TOTAL_BYTES, -} from '../../lib/shared/schemas/automations'; +} from '../../../lib/shared/schemas/automations'; import { isSkillBundleExcludedSegment, isValidSkillSlug, MAX_SKILL_BUNDLE_FILES, -} from '../../lib/shared/schemas/skills'; -import { parseSkillMd, SkillParseError } from '../../lib/skills/parse'; -import { isRecord } from '../../lib/utils/type-utils'; +} from '../../../lib/shared/schemas/skills'; +import { parseSkillMd, SkillParseError } from '../../../lib/skills/parse'; +import { isRecord } from '../../../lib/utils/type-utils'; const DOCUMENT_NAMES = new Set([ 'workflow.yml', diff --git a/services/platform/convex/automations/stepper.ts b/services/platform/backend/core/automations/stepper.ts similarity index 99% rename from services/platform/convex/automations/stepper.ts rename to services/platform/backend/core/automations/stepper.ts index 40fab1a4c5..30df86c266 100644 --- a/services/platform/convex/automations/stepper.ts +++ b/services/platform/backend/core/automations/stepper.ts @@ -1,27 +1,27 @@ 'use node'; -import { findConnector } from '../../lib/connectors/catalog'; -import { refsOf, topoSort } from '../../lib/engine/core/execute/controlflow'; +import { findConnector } from '../../../lib/connectors/catalog'; +import { refsOf, topoSort } from '../../../lib/engine/core/execute/controlflow'; import { cloneData, makeScope, mockAgentText, mockLlmText, stubFromSchema, -} from '../../lib/engine/core/execute/scope'; -import { hasCodeRunner, setCodeRunner } from '../../lib/engine/core/runner'; +} from '../../../lib/engine/core/execute/scope'; +import { hasCodeRunner, setCodeRunner } from '../../../lib/engine/core/runner'; import { evalCondition, evalTemplates, runCode, -} from '../../lib/engine/core/template'; +} from '../../../lib/engine/core/template'; import type { Effect, NodeDef, NodeTrace, Automation, -} from '../../lib/engine/core/types'; -import { nodeVmRunner } from '../../lib/engine/runners/node-vm'; +} from '../../../lib/engine/core/types'; +import { nodeVmRunner } from '../../../lib/engine/runners/node-vm'; import type { ActionCtx } from '../lib/ctx'; import { internal } from '../lib/handler_names'; import type { Id } from '../lib/rows'; diff --git a/services/platform/convex/automations/store.ts b/services/platform/backend/core/automations/store.ts similarity index 99% rename from services/platform/convex/automations/store.ts rename to services/platform/backend/core/automations/store.ts index 84578b687d..b1e6197e77 100644 --- a/services/platform/convex/automations/store.ts +++ b/services/platform/backend/core/automations/store.ts @@ -26,10 +26,13 @@ * therefore exist in exactly one place whatever the caller. */ -import type { DispatchStore, TriggerSpec } from '../../lib/engine/api/dispatch'; -import type { StoreAdapter } from '../../lib/engine/core/slots'; -import type { Automation, RunResult } from '../../lib/engine/core/types'; -import { AppError } from '../../lib/shared/errors/app-error'; +import type { + DispatchStore, + TriggerSpec, +} from '../../../lib/engine/api/dispatch'; +import type { StoreAdapter } from '../../../lib/engine/core/slots'; +import type { Automation, RunResult } from '../../../lib/engine/core/types'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { recordAutomationRunLedgerEntry } from '../audit_logs/agent_run_ledger'; import type { ActionCtx, MutationCtx, QueryCtx } from '../lib/ctx'; import { internal } from '../lib/handler_names'; diff --git a/services/platform/convex/automations/upload_impl.ts b/services/platform/backend/core/automations/upload_impl.ts similarity index 96% rename from services/platform/convex/automations/upload_impl.ts rename to services/platform/backend/core/automations/upload_impl.ts index 83389d70be..ddef83b89b 100644 --- a/services/platform/convex/automations/upload_impl.ts +++ b/services/platform/backend/core/automations/upload_impl.ts @@ -15,19 +15,19 @@ import { parse as parseYaml } from 'yaml'; import { automationPackManifestSchema, type AutomationPackManifest, -} from '../../lib/automations/packs'; -import { registerConnector } from '../../lib/connectors/registry'; -import { hasCodeRunner, setCodeRunner } from '../../lib/engine/core/runner'; -import { validate } from '../../lib/engine/core/validate'; -import { nodeVmRunner } from '../../lib/engine/runners/node-vm'; -import { AppError } from '../../lib/shared/errors/app-error'; -import { MAX_AUTOMATION_BUNDLE_TOTAL_BYTES } from '../../lib/shared/schemas/automations'; -import { readOrgSkill } from '../../lib/skills/listing'; +} from '../../../lib/automations/packs'; +import { registerConnector } from '../../../lib/connectors/registry'; +import { hasCodeRunner, setCodeRunner } from '../../../lib/engine/core/runner'; +import { validate } from '../../../lib/engine/core/validate'; +import { nodeVmRunner } from '../../../lib/engine/runners/node-vm'; +import { AppError } from '../../../lib/shared/errors/app-error'; +import { MAX_AUTOMATION_BUNDLE_TOTAL_BYTES } from '../../../lib/shared/schemas/automations'; +import { readOrgSkill } from '../../../lib/skills/listing'; import { canEditSkill, type UserSkillViewer, -} from '../../lib/skills/visibility'; -import { isRecord } from '../../lib/utils/type-utils'; +} from '../../../lib/skills/visibility'; +import { isRecord } from '../../../lib/utils/type-utils'; import { loadConnectorDefinitions } from '../connector_credentials/connector_catalog'; import { createOrgSkillReader, diff --git a/services/platform/convex/automations/webhook_token.ts b/services/platform/backend/core/automations/webhook_token.ts similarity index 100% rename from services/platform/convex/automations/webhook_token.ts rename to services/platform/backend/core/automations/webhook_token.ts diff --git a/services/platform/convex/automations_builder/chat_wire.test.ts b/services/platform/backend/core/automations_builder/chat_wire.test.ts similarity index 99% rename from services/platform/convex/automations_builder/chat_wire.test.ts rename to services/platform/backend/core/automations_builder/chat_wire.test.ts index aa0558a6e4..e058a7ad04 100644 --- a/services/platform/convex/automations_builder/chat_wire.test.ts +++ b/services/platform/backend/core/automations_builder/chat_wire.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { BuilderMessage } from '../../lib/automations_builder/session'; +import type { BuilderMessage } from '../../../lib/automations_builder/session'; import { buildChatRequest, parseChatReply } from './chat_wire'; const messages: BuilderMessage[] = [ diff --git a/services/platform/convex/automations_builder/chat_wire.ts b/services/platform/backend/core/automations_builder/chat_wire.ts similarity index 97% rename from services/platform/convex/automations_builder/chat_wire.ts rename to services/platform/backend/core/automations_builder/chat_wire.ts index 439e6a1961..f01ca00e54 100644 --- a/services/platform/convex/automations_builder/chat_wire.ts +++ b/services/platform/backend/core/automations_builder/chat_wire.ts @@ -17,14 +17,14 @@ * costs a turn. */ -import { asRecord } from '../../lib/automations_builder/results'; -import type { TurnSampling } from '../../lib/chat/effort'; -import type { WireTool } from '../../lib/chat/tools'; -import type { ChatWireMessage } from '../../lib/chat/wire-parts'; +import { asRecord } from '../../../lib/automations_builder/results'; +import type { TurnSampling } from '../../../lib/chat/effort'; +import type { WireTool } from '../../../lib/chat/tools'; +import type { ChatWireMessage } from '../../../lib/chat/wire-parts'; import type { ApiFormat, WireDialect, -} from '../../lib/shared/schemas/providers'; +} from '../../../lib/shared/schemas/providers'; export interface ChatWireRequest { url: string; @@ -41,7 +41,7 @@ export type { ChatWireMessage, WireToolCall, WireToolResult, -} from '../../lib/chat/wire-parts'; +} from '../../../lib/chat/wire-parts'; export interface ChatWireArgs { apiFormat: ApiFormat; diff --git a/services/platform/convex/automations_builder/mcp_http.test.ts b/services/platform/backend/core/automations_builder/mcp_http.test.ts similarity index 99% rename from services/platform/convex/automations_builder/mcp_http.test.ts rename to services/platform/backend/core/automations_builder/mcp_http.test.ts index 20b71b8cff..09456a6360 100644 --- a/services/platform/convex/automations_builder/mcp_http.test.ts +++ b/services/platform/backend/core/automations_builder/mcp_http.test.ts @@ -15,7 +15,7 @@ import { describe, expect, it, vi } from 'vitest'; -import { MCP_TOOLS } from '../../lib/mcp/tools'; +import { MCP_TOOLS } from '../../../lib/mcp/tools'; import { internal } from '../lib/handler_names'; import type { RestContext } from '../lib/rest/helpers'; import { handleMcpRequest, mcpGetNotAllowed } from './mcp_http'; diff --git a/services/platform/convex/automations_builder/mcp_http.ts b/services/platform/backend/core/automations_builder/mcp_http.ts similarity index 98% rename from services/platform/convex/automations_builder/mcp_http.ts rename to services/platform/backend/core/automations_builder/mcp_http.ts index d548179330..3e8fcba1ee 100644 --- a/services/platform/convex/automations_builder/mcp_http.ts +++ b/services/platform/backend/core/automations_builder/mcp_http.ts @@ -34,8 +34,8 @@ * the streamable-HTTP transport specifies. */ -import { MCP_TOOLS, mcpToolKind } from '../../lib/mcp/tools'; -import { AppError } from '../../lib/shared/errors/app-error'; +import { MCP_TOOLS, mcpToolKind } from '../../../lib/mcp/tools'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { internal } from '../lib/handler_names'; import { jsonError, diff --git a/services/platform/convex/automations_builder/model_call.ts b/services/platform/backend/core/automations_builder/model_call.ts similarity index 95% rename from services/platform/convex/automations_builder/model_call.ts rename to services/platform/backend/core/automations_builder/model_call.ts index 6034318801..32b0462513 100644 --- a/services/platform/convex/automations_builder/model_call.ts +++ b/services/platform/backend/core/automations_builder/model_call.ts @@ -19,16 +19,16 @@ * secret on every turn buys nothing. */ -import type { BuilderModel } from '../../lib/automations_builder/session'; -import { AppError } from '../../lib/shared/errors/app-error'; -import { providerAttributionHeaders } from '../../lib/shared/providers/attribution'; +import type { BuilderModel } from '../../../lib/automations_builder/session'; +import { safeFetch, SafeFetchError } from '../../../lib/net/safe-fetch'; +import { AppError } from '../../../lib/shared/errors/app-error'; +import { providerAttributionHeaders } from '../../../lib/shared/providers/attribution'; import type { ApiFormat, ModelCatalogEntry, WireDialect, -} from '../../lib/shared/schemas/providers'; +} from '../../../lib/shared/schemas/providers'; import type { ActionCtx } from '../lib/ctx'; -import { safeFetch, SafeFetchError } from '../lib/http/safe_fetch'; import { getProviderCatalog } from '../lib/providers/catalog_fetch'; import { resolveProvidersForOrgId } from '../lib/providers/org_providers'; import { sanitizeError } from '../lib/utils/sanitize_secrets'; diff --git a/services/platform/convex/automations_builder/run_session.ts b/services/platform/backend/core/automations_builder/run_session.ts similarity index 90% rename from services/platform/convex/automations_builder/run_session.ts rename to services/platform/backend/core/automations_builder/run_session.ts index fcd0a70674..d1953866b6 100644 --- a/services/platform/convex/automations_builder/run_session.ts +++ b/services/platform/backend/core/automations_builder/run_session.ts @@ -21,12 +21,12 @@ * system. Live runs belong to deployment, behind the deploy gate. */ -import { runBuilderSession } from '../../lib/automations_builder/session'; -import { installConnectorCatalog } from '../../lib/connectors/dispatcher'; -import { registerConnector } from '../../lib/connectors/registry'; -import { dispatch, type DispatchStore } from '../../lib/engine/api/dispatch'; -import { hasCodeRunner, setCodeRunner } from '../../lib/engine/core/runner'; -import { nodeVmRunner } from '../../lib/engine/runners/node-vm'; +import { runBuilderSession } from '../../../lib/automations_builder/session'; +import { installConnectorCatalog } from '../../../lib/connectors/dispatcher'; +import { registerConnector } from '../../../lib/connectors/registry'; +import { dispatch, type DispatchStore } from '../../../lib/engine/api/dispatch'; +import { hasCodeRunner, setCodeRunner } from '../../../lib/engine/core/runner'; +import { nodeVmRunner } from '../../../lib/engine/runners/node-vm'; import { loadConnectorDefinitions } from '../connector_credentials/connector_catalog'; import type { ActionCtx } from '../lib/ctx'; import { createBuilderModel, type BuilderModelTarget } from './model_call'; diff --git a/services/platform/convex/betterAuth/trusted_headers/get_user_by_id.ts b/services/platform/backend/core/betterAuth/trusted_headers/get_user_by_id.ts similarity index 97% rename from services/platform/convex/betterAuth/trusted_headers/get_user_by_id.ts rename to services/platform/backend/core/betterAuth/trusted_headers/get_user_by_id.ts index 3f3ca9b524..21c0594077 100644 --- a/services/platform/convex/betterAuth/trusted_headers/get_user_by_id.ts +++ b/services/platform/backend/core/betterAuth/trusted_headers/get_user_by_id.ts @@ -7,7 +7,7 @@ import { getString, getNumber, getBoolean, -} from '../../../lib/utils/type-utils'; +} from '../../../../lib/utils/type-utils'; import type { QueryCtx } from '../../lib/ctx'; import { components } from '../../lib/handler_names'; import { looksLikeConvexDocumentId } from '../../lib/helpers/id_shape'; diff --git a/services/platform/convex/betterAuth/trusted_headers/resolve_team_names.ts b/services/platform/backend/core/betterAuth/trusted_headers/resolve_team_names.ts similarity index 100% rename from services/platform/convex/betterAuth/trusted_headers/resolve_team_names.ts rename to services/platform/backend/core/betterAuth/trusted_headers/resolve_team_names.ts diff --git a/services/platform/convex/branding/file_utils.ts b/services/platform/backend/core/branding/file_utils.ts similarity index 97% rename from services/platform/convex/branding/file_utils.ts rename to services/platform/backend/core/branding/file_utils.ts index 31a929d6d6..1608da488d 100644 --- a/services/platform/convex/branding/file_utils.ts +++ b/services/platform/backend/core/branding/file_utils.ts @@ -12,8 +12,8 @@ import path from 'node:path'; import { brandingJsonSchema, type BrandingJsonConfig, -} from '../../lib/shared/schemas/branding'; -import { zodErrorMessage } from '../../lib/shared/schemas/format-error'; +} from '../../../lib/shared/schemas/branding'; +import { zodErrorMessage } from '../../../lib/shared/schemas/format-error'; import { getConfigRoot, safeJoinWithinDir, diff --git a/services/platform/convex/changelog/internal_actions.ts b/services/platform/backend/core/changelog/internal_actions.ts similarity index 100% rename from services/platform/convex/changelog/internal_actions.ts rename to services/platform/backend/core/changelog/internal_actions.ts diff --git a/services/platform/convex/chat/assistant_tools.test.ts b/services/platform/backend/core/chat/assistant_tools.test.ts similarity index 99% rename from services/platform/convex/chat/assistant_tools.test.ts rename to services/platform/backend/core/chat/assistant_tools.test.ts index 292c79d625..aeeb362ef8 100644 --- a/services/platform/convex/chat/assistant_tools.test.ts +++ b/services/platform/backend/core/chat/assistant_tools.test.ts @@ -15,9 +15,9 @@ import { RAG_SEARCH_ENTITY_LIMIT, RAG_SEARCH_MAX_LIMIT, RAG_SEARCH_MIN_SIMILARITY, -} from '../../lib/chat'; -import { functionRefName } from '../../lib/shared/handlers/function-refs'; -import { SafeFetchError } from '../lib/http/safe_fetch'; +} from '../../../lib/chat'; +import { SafeFetchError } from '../../../lib/net/safe-fetch'; +import { functionRefName } from '../../../lib/shared/handlers/function-refs'; const searchKnowledgeMock = vi.fn(); vi.mock('../knowledge/search', () => ({ @@ -45,7 +45,7 @@ vi.mock('../lib/helpers/org_slug', () => ({ // `safeFetch` is the only network edge; `isPrivateIp` and `SafeFetchError` // stay real so the URL policy under test is the shipped one. const safeFetchMock = vi.fn(); -vi.mock('../lib/http/safe_fetch', async (importOriginal) => { +vi.mock('../../../lib/net/safe-fetch', async (importOriginal) => { const mod = await importOriginal>(); return { ...mod, diff --git a/services/platform/convex/chat/assistant_tools.ts b/services/platform/backend/core/chat/assistant_tools.ts similarity index 99% rename from services/platform/convex/chat/assistant_tools.ts rename to services/platform/backend/core/chat/assistant_tools.ts index a08219b967..e06c4a0155 100644 --- a/services/platform/convex/chat/assistant_tools.ts +++ b/services/platform/backend/core/chat/assistant_tools.ts @@ -37,13 +37,22 @@ import { type ChatToolExecutor, type RagSearchKind, type ToolCallRequest, -} from '../../lib/chat'; -import { htmlTitle, htmlToText } from '../../lib/knowledge/html-to-text'; +} from '../../../lib/chat'; +import { + sanitizeUntrustedField, + wrapUntrusted, +} from '../../../lib/chat/untrusted-content'; +import { htmlTitle, htmlToText } from '../../../lib/knowledge/html-to-text'; import { knowledgeScopeAllows, type KnowledgeAccessScope, -} from '../../lib/knowledge/types'; -import { modelTimestamp } from '../../lib/shared/model-timestamp'; +} from '../../../lib/knowledge/types'; +import { + SafeFetchError, + isPrivateIp, + safeFetch, +} from '../../../lib/net/safe-fetch'; +import { modelTimestamp } from '../../../lib/shared/model-timestamp'; import { FETCH_WINDOW_CHARS, fetchDocumentByFileId, @@ -54,14 +63,9 @@ import { searchKnowledge } from '../knowledge/search'; import type { ActionCtx } from '../lib/ctx'; import { internal } from '../lib/handler_names'; import { orgSlugFromId } from '../lib/helpers/org_slug'; -import { SafeFetchError, isPrivateIp, safeFetch } from '../lib/http/safe_fetch'; import type { AgentReadSubject } from '../lib/rls/helpers/agent_read_access'; import type { Doc } from '../lib/rows'; import { detectListingIntent } from '../lib/search'; -import { - sanitizeUntrustedField, - wrapUntrusted, -} from '../lib/untrusted_content'; /** Who the tools run for. The user is re-checked per dispatch. */ export interface ChatToolContext { diff --git a/services/platform/convex/chat/composer.ts b/services/platform/backend/core/chat/composer.ts similarity index 100% rename from services/platform/convex/chat/composer.ts rename to services/platform/backend/core/chat/composer.ts diff --git a/services/platform/convex/chat/external_turn_shared.ts b/services/platform/backend/core/chat/external_turn_shared.ts similarity index 99% rename from services/platform/convex/chat/external_turn_shared.ts rename to services/platform/backend/core/chat/external_turn_shared.ts index bcd72a43b0..297e895688 100644 --- a/services/platform/convex/chat/external_turn_shared.ts +++ b/services/platform/backend/core/chat/external_turn_shared.ts @@ -23,16 +23,16 @@ * with its own token mint, progress sink, and settle. */ -import { getHarnessGlue } from '../../lib/harnesses/registry'; +import { getHarnessGlue } from '../../../lib/harnesses/registry'; import { boundTimelineParts, type TimelinePart, -} from '../../lib/harnesses/timeline'; +} from '../../../lib/harnesses/timeline'; import { isHarnessSlug, type HarnessEvent, type HarnessExec, -} from '../../lib/harnesses/types'; +} from '../../../lib/harnesses/types'; import { loadHarnesses } from '../lib/providers/load_system_config'; import { drainSessionExecResilient, diff --git a/services/platform/convex/chat/generate_title.ts b/services/platform/backend/core/chat/generate_title.ts similarity index 98% rename from services/platform/convex/chat/generate_title.ts rename to services/platform/backend/core/chat/generate_title.ts index 87bb3678c5..ecab51dd99 100644 --- a/services/platform/convex/chat/generate_title.ts +++ b/services/platform/backend/core/chat/generate_title.ts @@ -1,7 +1,7 @@ 'use node'; -import { deriveFallbackTitle } from '../../lib/chat/derive-fallback-title'; -import type { ModelCatalogEntry } from '../../lib/shared/schemas/providers'; +import { deriveFallbackTitle } from '../../../lib/chat/derive-fallback-title'; +import type { ModelCatalogEntry } from '../../../lib/shared/schemas/providers'; import { createBuilderModel } from '../automations_builder/model_call'; import type { ActionCtx } from '../lib/ctx'; import { internal } from '../lib/handler_names'; diff --git a/services/platform/convex/chat/project_context.test.ts b/services/platform/backend/core/chat/project_context.test.ts similarity index 98% rename from services/platform/convex/chat/project_context.test.ts rename to services/platform/backend/core/chat/project_context.test.ts index ea860d23cc..8f83c01a2b 100644 --- a/services/platform/convex/chat/project_context.test.ts +++ b/services/platform/backend/core/chat/project_context.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { functionRefName } from '../../lib/shared/handlers/function-refs'; +import { functionRefName } from '../../../lib/shared/handlers/function-refs'; import { resolveProjectContext } from './project_context'; /** diff --git a/services/platform/convex/chat/project_context.ts b/services/platform/backend/core/chat/project_context.ts similarity index 96% rename from services/platform/convex/chat/project_context.ts rename to services/platform/backend/core/chat/project_context.ts index 29739beb9f..0ebc9a19e4 100644 --- a/services/platform/convex/chat/project_context.ts +++ b/services/platform/backend/core/chat/project_context.ts @@ -1,4 +1,4 @@ -import type { ProjectContext } from '../../lib/chat/context'; +import type { ProjectContext } from '../../../lib/chat/context'; import type { ActionCtx } from '../lib/ctx'; import { internal } from '../lib/handler_names'; import type { Id } from '../lib/rows'; diff --git a/services/platform/convex/chat/turn_action.ts b/services/platform/backend/core/chat/turn_action.ts similarity index 97% rename from services/platform/convex/chat/turn_action.ts rename to services/platform/backend/core/chat/turn_action.ts index 91e898dfc7..3115425c19 100644 --- a/services/platform/convex/chat/turn_action.ts +++ b/services/platform/backend/core/chat/turn_action.ts @@ -1,19 +1,19 @@ 'use node'; -import { CHAT_ASSISTANT } from '../../lib/chat/assistant'; +import { CHAT_ASSISTANT } from '../../../lib/chat/assistant'; import { buildAudioTranscriptAppendix, stripAudioTranscriptAppendix, -} from '../../lib/chat/audio-transcript'; -import { resolveEffectiveWindow } from '../../lib/chat/budget'; -import { buildDocumentAppendix } from '../../lib/chat/document-appendix'; +} from '../../../lib/chat/audio-transcript'; +import { resolveEffectiveWindow } from '../../../lib/chat/budget'; +import { buildDocumentAppendix } from '../../../lib/chat/document-appendix'; import { fitSamplingToWindow, resolveTurnSampling, type ReasoningEffort, -} from '../../lib/chat/effort'; -import { CHAT_TOOL_DOCS, type ToolCallRequest } from '../../lib/chat/tools'; -import { runTurn, userTurnParts } from '../../lib/chat/turn'; +} from '../../../lib/chat/effort'; +import { CHAT_TOOL_DOCS, type ToolCallRequest } from '../../../lib/chat/tools'; +import { runTurn, userTurnParts } from '../../../lib/chat/turn'; import type { ModelCall, ModelStreamChunk, @@ -21,43 +21,43 @@ import type { TurnDeps, TurnOutcome, TurnRequest, -} from '../../lib/chat/turn'; +} from '../../../lib/chat/turn'; import { messageText, type ChatMessage, type MessagePart, type TurnUsage, -} from '../../lib/chat/types'; +} from '../../../lib/chat/types'; import { explodeMessagesForWire, type ChatWireMessage, type WireImage, -} from '../../lib/chat/wire-parts'; -import { AppError } from '../../lib/shared/errors/app-error'; +} from '../../../lib/chat/wire-parts'; +import { checkProviderHostPolicy } from '../../../lib/net/host-policy'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { CHAT_MAX_FILE_COUNT, CHAT_UPLOAD_ALLOWED_TYPES, isAudioOrVideo, isDocument, isImage, -} from '../../lib/shared/file-types'; -import { providerAttributionHeaders } from '../../lib/shared/providers/attribution'; +} from '../../../lib/shared/file-types'; +import { providerAttributionHeaders } from '../../../lib/shared/providers/attribution'; import { buildHarnessTable, type CredentialAuth, -} from '../../lib/shared/providers/resolve_execution'; +} from '../../../lib/shared/providers/resolve_execution'; import type { ApiFormat, ModelCatalogEntry, ProviderDefinition, WireDialect, -} from '../../lib/shared/schemas/providers'; -import { isTextBasedFile } from '../../lib/utils/text-file-types'; +} from '../../../lib/shared/schemas/providers'; +import { isTextBasedFile } from '../../../lib/utils/text-file-types'; import { buildChatRequest } from '../automations_builder/chat_wire'; import type { ActionCtx } from '../lib/ctx'; import { internal } from '../lib/handler_names'; import { orgSlugFromIdOrNull } from '../lib/helpers/org_slug'; -import { checkProviderHostPolicy } from '../lib/http/host_policy'; import { getProviderCatalog } from '../lib/providers/catalog_fetch'; import { loadHarnesses } from '../lib/providers/load_system_config'; import { resolveProvidersForOrgId } from '../lib/providers/org_providers'; diff --git a/services/platform/convex/chat/turn_store.ts b/services/platform/backend/core/chat/turn_store.ts similarity index 97% rename from services/platform/convex/chat/turn_store.ts rename to services/platform/backend/core/chat/turn_store.ts index 831fe348ac..fcffab0792 100644 --- a/services/platform/convex/chat/turn_store.ts +++ b/services/platform/backend/core/chat/turn_store.ts @@ -14,9 +14,9 @@ * without a Node runtime. */ -import { estimateCostCents } from '../../lib/chat/turn'; -import type { TurnStore, UsageLedger } from '../../lib/chat/turn'; -import type { ModelCatalogEntry } from '../../lib/shared/schemas/providers'; +import { estimateCostCents } from '../../../lib/chat/turn'; +import type { TurnStore, UsageLedger } from '../../../lib/chat/turn'; +import type { ModelCatalogEntry } from '../../../lib/shared/schemas/providers'; import type { ActionCtx } from '../lib/ctx'; import { internal } from '../lib/handler_names'; diff --git a/services/platform/convex/cloud_import/deployment_config.test.ts b/services/platform/backend/core/cloud_import/deployment_config.test.ts similarity index 100% rename from services/platform/convex/cloud_import/deployment_config.test.ts rename to services/platform/backend/core/cloud_import/deployment_config.test.ts diff --git a/services/platform/convex/cloud_import/deployment_config.ts b/services/platform/backend/core/cloud_import/deployment_config.ts similarity index 100% rename from services/platform/convex/cloud_import/deployment_config.ts rename to services/platform/backend/core/cloud_import/deployment_config.ts diff --git a/services/platform/convex/cloud_import/providers.ts b/services/platform/backend/core/cloud_import/providers.ts similarity index 100% rename from services/platform/convex/cloud_import/providers.ts rename to services/platform/backend/core/cloud_import/providers.ts diff --git a/services/platform/convex/cloud_import/token_refresh.ts b/services/platform/backend/core/cloud_import/token_refresh.ts similarity index 96% rename from services/platform/convex/cloud_import/token_refresh.ts rename to services/platform/backend/core/cloud_import/token_refresh.ts index 83b49e5337..39c1b7f7ae 100644 --- a/services/platform/convex/cloud_import/token_refresh.ts +++ b/services/platform/backend/core/cloud_import/token_refresh.ts @@ -6,7 +6,7 @@ * needs-reauth marking. */ -import { getNumber, getString, isRecord } from '../../lib/utils/type-utils'; +import { getNumber, getString, isRecord } from '../../../lib/utils/type-utils'; import { microsoftCloudImportOauthUrls } from './deployment_config'; import { getCloudImportProviderEndpoints } from './providers'; diff --git a/services/platform/convex/cloud_import/types.ts b/services/platform/backend/core/cloud_import/types.ts similarity index 100% rename from services/platform/convex/cloud_import/types.ts rename to services/platform/backend/core/cloud_import/types.ts diff --git a/services/platform/convex/collab/coalesce.ts b/services/platform/backend/core/collab/coalesce.ts similarity index 99% rename from services/platform/convex/collab/coalesce.ts rename to services/platform/backend/core/collab/coalesce.ts index 1ace9ae66e..b6e838e813 100644 --- a/services/platform/convex/collab/coalesce.ts +++ b/services/platform/backend/core/collab/coalesce.ts @@ -23,7 +23,7 @@ * of their history, so the next event starts a fresh row. */ -import { isActionableNotificationType } from '../../lib/shared/attention'; +import { isActionableNotificationType } from '../../../lib/shared/attention'; import type { MutationCtx } from '../lib/ctx'; import { internal } from '../lib/handler_names'; import type { Doc, Id } from '../lib/rows'; diff --git a/services/platform/convex/collab/dismiss_review_notifications.ts b/services/platform/backend/core/collab/dismiss_review_notifications.ts similarity index 98% rename from services/platform/convex/collab/dismiss_review_notifications.ts rename to services/platform/backend/core/collab/dismiss_review_notifications.ts index 29aeda44d6..fe3226bed6 100644 --- a/services/platform/convex/collab/dismiss_review_notifications.ts +++ b/services/platform/backend/core/collab/dismiss_review_notifications.ts @@ -4,7 +4,7 @@ * reminders (resourceId = taskId, params.approvalId), and admin escalations. */ -import { isRecord } from '../../lib/utils/type-utils'; +import { isRecord } from '../../../lib/utils/type-utils'; import type { MutationCtx } from '../lib/ctx'; import type { Id } from '../lib/rows'; diff --git a/services/platform/convex/collab/notify_task_reviews.ts b/services/platform/backend/core/collab/notify_task_reviews.ts similarity index 100% rename from services/platform/convex/collab/notify_task_reviews.ts rename to services/platform/backend/core/collab/notify_task_reviews.ts diff --git a/services/platform/convex/collab/types.ts b/services/platform/backend/core/collab/types.ts similarity index 100% rename from services/platform/convex/collab/types.ts rename to services/platform/backend/core/collab/types.ts diff --git a/services/platform/convex/connector_credentials/auth_injection.test.ts b/services/platform/backend/core/connector_credentials/auth_injection.test.ts similarity index 100% rename from services/platform/convex/connector_credentials/auth_injection.test.ts rename to services/platform/backend/core/connector_credentials/auth_injection.test.ts diff --git a/services/platform/convex/connector_credentials/auth_injection.ts b/services/platform/backend/core/connector_credentials/auth_injection.ts similarity index 100% rename from services/platform/convex/connector_credentials/auth_injection.ts rename to services/platform/backend/core/connector_credentials/auth_injection.ts diff --git a/services/platform/convex/connector_credentials/connector_catalog.ts b/services/platform/backend/core/connector_credentials/connector_catalog.ts similarity index 99% rename from services/platform/convex/connector_credentials/connector_catalog.ts rename to services/platform/backend/core/connector_credentials/connector_catalog.ts index 91ebd48de6..27738c9445 100644 --- a/services/platform/convex/connector_credentials/connector_catalog.ts +++ b/services/platform/backend/core/connector_credentials/connector_catalog.ts @@ -25,7 +25,7 @@ import { loadConnectorDefinitions, resolveConnectorsDir, type LoadConnectorCatalogOptions, -} from '../../lib/connectors/catalog'; +} from '../../../lib/connectors/catalog'; import type { ConnectorAuthMethod } from './types'; export { connectorBearerScheme, findConnector, loadConnectorDefinitions }; diff --git a/services/platform/convex/connector_credentials/imap_from_address.test.ts b/services/platform/backend/core/connector_credentials/imap_from_address.test.ts similarity index 100% rename from services/platform/convex/connector_credentials/imap_from_address.test.ts rename to services/platform/backend/core/connector_credentials/imap_from_address.test.ts diff --git a/services/platform/convex/connector_credentials/imap_from_address.ts b/services/platform/backend/core/connector_credentials/imap_from_address.ts similarity index 100% rename from services/platform/convex/connector_credentials/imap_from_address.ts rename to services/platform/backend/core/connector_credentials/imap_from_address.ts diff --git a/services/platform/convex/connector_credentials/masking.test.ts b/services/platform/backend/core/connector_credentials/masking.test.ts similarity index 100% rename from services/platform/convex/connector_credentials/masking.test.ts rename to services/platform/backend/core/connector_credentials/masking.test.ts diff --git a/services/platform/convex/connector_credentials/masking.ts b/services/platform/backend/core/connector_credentials/masking.ts similarity index 100% rename from services/platform/convex/connector_credentials/masking.ts rename to services/platform/backend/core/connector_credentials/masking.ts diff --git a/services/platform/convex/connector_credentials/mutations.ts b/services/platform/backend/core/connector_credentials/mutations.ts similarity index 96% rename from services/platform/convex/connector_credentials/mutations.ts rename to services/platform/backend/core/connector_credentials/mutations.ts index 4794d1c070..927449cba7 100644 --- a/services/platform/convex/connector_credentials/mutations.ts +++ b/services/platform/backend/core/connector_credentials/mutations.ts @@ -1,4 +1,4 @@ -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; /** * Canonical form of a per-credential API origin: https, no trailing slash, diff --git a/services/platform/convex/connector_credentials/resolve_credential.ts b/services/platform/backend/core/connector_credentials/resolve_credential.ts similarity index 99% rename from services/platform/convex/connector_credentials/resolve_credential.ts rename to services/platform/backend/core/connector_credentials/resolve_credential.ts index 1b0d6fec93..531ffb9602 100644 --- a/services/platform/convex/connector_credentials/resolve_credential.ts +++ b/services/platform/backend/core/connector_credentials/resolve_credential.ts @@ -25,7 +25,7 @@ * envelope each say what to do next, and say it differently. */ -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import type { ActionCtx } from '../lib/ctx'; import { internal } from '../lib/handler_names'; import type { Id } from '../lib/rows'; diff --git a/services/platform/convex/connector_credentials/types.ts b/services/platform/backend/core/connector_credentials/types.ts similarity index 100% rename from services/platform/convex/connector_credentials/types.ts rename to services/platform/backend/core/connector_credentials/types.ts diff --git a/services/platform/convex/connectors/hostcall_token.test.ts b/services/platform/backend/core/connectors/hostcall_token.test.ts similarity index 100% rename from services/platform/convex/connectors/hostcall_token.test.ts rename to services/platform/backend/core/connectors/hostcall_token.test.ts diff --git a/services/platform/convex/connectors/hostcall_token.ts b/services/platform/backend/core/connectors/hostcall_token.ts similarity index 100% rename from services/platform/convex/connectors/hostcall_token.ts rename to services/platform/backend/core/connectors/hostcall_token.ts diff --git a/services/platform/convex/conversations/README.md b/services/platform/backend/core/conversations/README.md similarity index 100% rename from services/platform/convex/conversations/README.md rename to services/platform/backend/core/conversations/README.md diff --git a/services/platform/convex/conversations/attachments.test.ts b/services/platform/backend/core/conversations/attachments.test.ts similarity index 97% rename from services/platform/convex/conversations/attachments.test.ts rename to services/platform/backend/core/conversations/attachments.test.ts index 0658e79690..07006b81aa 100644 --- a/services/platform/convex/conversations/attachments.test.ts +++ b/services/platform/backend/core/conversations/attachments.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { type ConversationAttachmentCapInput, validateConversationAttachmentCaps, diff --git a/services/platform/convex/conversations/attachments.ts b/services/platform/backend/core/conversations/attachments.ts similarity index 95% rename from services/platform/convex/conversations/attachments.ts rename to services/platform/backend/core/conversations/attachments.ts index 0614b64468..91f76c8200 100644 --- a/services/platform/convex/conversations/attachments.ts +++ b/services/platform/backend/core/conversations/attachments.ts @@ -24,8 +24,8 @@ import { CHAT_UPLOAD_ALLOWED_TYPES, getMaxFileSizeForType, validateAttachmentCaps, -} from '../../lib/shared/file-types'; -import { isTextBasedFile } from '../../lib/utils/text-file-types'; +} from '../../../lib/shared/file-types'; +import { isTextBasedFile } from '../../../lib/utils/text-file-types'; export interface ConversationAttachmentCapInput { fileName: string; diff --git a/services/platform/convex/conversations/build_threading_headers.test.ts b/services/platform/backend/core/conversations/build_threading_headers.test.ts similarity index 100% rename from services/platform/convex/conversations/build_threading_headers.test.ts rename to services/platform/backend/core/conversations/build_threading_headers.test.ts diff --git a/services/platform/convex/conversations/build_threading_headers.ts b/services/platform/backend/core/conversations/build_threading_headers.ts similarity index 100% rename from services/platform/convex/conversations/build_threading_headers.ts rename to services/platform/backend/core/conversations/build_threading_headers.ts diff --git a/services/platform/convex/conversations/connector_slug.test.ts b/services/platform/backend/core/conversations/connector_slug.test.ts similarity index 100% rename from services/platform/convex/conversations/connector_slug.test.ts rename to services/platform/backend/core/conversations/connector_slug.test.ts diff --git a/services/platform/convex/conversations/connector_slug.ts b/services/platform/backend/core/conversations/connector_slug.ts similarity index 100% rename from services/platform/convex/conversations/connector_slug.ts rename to services/platform/backend/core/conversations/connector_slug.ts diff --git a/services/platform/convex/conversations/ingest/add_message_to_conversation.ts b/services/platform/backend/core/conversations/ingest/add_message_to_conversation.ts similarity index 100% rename from services/platform/convex/conversations/ingest/add_message_to_conversation.ts rename to services/platform/backend/core/conversations/ingest/add_message_to_conversation.ts diff --git a/services/platform/convex/conversations/ingest/attachments_for_metadata.test.ts b/services/platform/backend/core/conversations/ingest/attachments_for_metadata.test.ts similarity index 100% rename from services/platform/convex/conversations/ingest/attachments_for_metadata.test.ts rename to services/platform/backend/core/conversations/ingest/attachments_for_metadata.test.ts diff --git a/services/platform/convex/conversations/ingest/attachments_for_metadata.ts b/services/platform/backend/core/conversations/ingest/attachments_for_metadata.ts similarity index 100% rename from services/platform/convex/conversations/ingest/attachments_for_metadata.ts rename to services/platform/backend/core/conversations/ingest/attachments_for_metadata.ts diff --git a/services/platform/convex/conversations/ingest/bind_email_attachments.test.ts b/services/platform/backend/core/conversations/ingest/bind_email_attachments.test.ts similarity index 100% rename from services/platform/convex/conversations/ingest/bind_email_attachments.test.ts rename to services/platform/backend/core/conversations/ingest/bind_email_attachments.test.ts diff --git a/services/platform/convex/conversations/ingest/bind_email_attachments.ts b/services/platform/backend/core/conversations/ingest/bind_email_attachments.ts similarity index 98% rename from services/platform/convex/conversations/ingest/bind_email_attachments.ts rename to services/platform/backend/core/conversations/ingest/bind_email_attachments.ts index b59b23b1b8..ca8568252b 100644 --- a/services/platform/convex/conversations/ingest/bind_email_attachments.ts +++ b/services/platform/backend/core/conversations/ingest/bind_email_attachments.ts @@ -34,7 +34,7 @@ * fixes both together. */ -import { isRecord } from '../../../lib/utils/type-utils'; +import { isRecord } from '../../../../lib/utils/type-utils'; import type { ActionCtx } from '../../lib/ctx'; import { internal } from '../../lib/handler_names'; import type { Id } from '../../lib/rows'; diff --git a/services/platform/convex/conversations/ingest/build_conversation_metadata.ts b/services/platform/backend/core/conversations/ingest/build_conversation_metadata.ts similarity index 100% rename from services/platform/convex/conversations/ingest/build_conversation_metadata.ts rename to services/platform/backend/core/conversations/ingest/build_conversation_metadata.ts diff --git a/services/platform/convex/conversations/ingest/build_email_metadata.ts b/services/platform/backend/core/conversations/ingest/build_email_metadata.ts similarity index 100% rename from services/platform/convex/conversations/ingest/build_email_metadata.ts rename to services/platform/backend/core/conversations/ingest/build_email_metadata.ts diff --git a/services/platform/convex/conversations/ingest/build_initial_message.ts b/services/platform/backend/core/conversations/ingest/build_initial_message.ts similarity index 100% rename from services/platform/convex/conversations/ingest/build_initial_message.ts rename to services/platform/backend/core/conversations/ingest/build_initial_message.ts diff --git a/services/platform/convex/conversations/ingest/check_conversation_exists.ts b/services/platform/backend/core/conversations/ingest/check_conversation_exists.ts similarity index 100% rename from services/platform/convex/conversations/ingest/check_conversation_exists.ts rename to services/platform/backend/core/conversations/ingest/check_conversation_exists.ts diff --git a/services/platform/convex/conversations/ingest/check_message_exists.ts b/services/platform/backend/core/conversations/ingest/check_message_exists.ts similarity index 100% rename from services/platform/convex/conversations/ingest/check_message_exists.ts rename to services/platform/backend/core/conversations/ingest/check_message_exists.ts diff --git a/services/platform/convex/conversations/ingest/constants.ts b/services/platform/backend/core/conversations/ingest/constants.ts similarity index 100% rename from services/platform/convex/conversations/ingest/constants.ts rename to services/platform/backend/core/conversations/ingest/constants.ts diff --git a/services/platform/convex/conversations/ingest/create_conversation_from_email.test.ts b/services/platform/backend/core/conversations/ingest/create_conversation_from_email.test.ts similarity index 100% rename from services/platform/convex/conversations/ingest/create_conversation_from_email.test.ts rename to services/platform/backend/core/conversations/ingest/create_conversation_from_email.test.ts diff --git a/services/platform/convex/conversations/ingest/create_conversation_from_email.ts b/services/platform/backend/core/conversations/ingest/create_conversation_from_email.ts similarity index 99% rename from services/platform/convex/conversations/ingest/create_conversation_from_email.ts rename to services/platform/backend/core/conversations/ingest/create_conversation_from_email.ts index 2965ea502f..016be1e005 100644 --- a/services/platform/convex/conversations/ingest/create_conversation_from_email.ts +++ b/services/platform/backend/core/conversations/ingest/create_conversation_from_email.ts @@ -1,4 +1,4 @@ -import { isRecord, getString } from '../../../lib/utils/type-utils'; +import { isRecord, getString } from '../../../../lib/utils/type-utils'; import type { ActionCtx } from '../../lib/ctx'; import { createDebugLog } from '../../lib/debug_log'; import { internal } from '../../lib/handler_names'; diff --git a/services/platform/convex/conversations/ingest/create_conversation_from_sent_email.test.ts b/services/platform/backend/core/conversations/ingest/create_conversation_from_sent_email.test.ts similarity index 100% rename from services/platform/convex/conversations/ingest/create_conversation_from_sent_email.test.ts rename to services/platform/backend/core/conversations/ingest/create_conversation_from_sent_email.test.ts diff --git a/services/platform/convex/conversations/ingest/create_conversation_from_sent_email.ts b/services/platform/backend/core/conversations/ingest/create_conversation_from_sent_email.ts similarity index 100% rename from services/platform/convex/conversations/ingest/create_conversation_from_sent_email.ts rename to services/platform/backend/core/conversations/ingest/create_conversation_from_sent_email.ts diff --git a/services/platform/convex/conversations/ingest/find_or_create_contact_from_email.test.ts b/services/platform/backend/core/conversations/ingest/find_or_create_contact_from_email.test.ts similarity index 100% rename from services/platform/convex/conversations/ingest/find_or_create_contact_from_email.test.ts rename to services/platform/backend/core/conversations/ingest/find_or_create_contact_from_email.test.ts diff --git a/services/platform/convex/conversations/ingest/find_or_create_contact_from_email.ts b/services/platform/backend/core/conversations/ingest/find_or_create_contact_from_email.ts similarity index 100% rename from services/platform/convex/conversations/ingest/find_or_create_contact_from_email.ts rename to services/platform/backend/core/conversations/ingest/find_or_create_contact_from_email.ts diff --git a/services/platform/convex/conversations/ingest/materialize_email_attachments.test.ts b/services/platform/backend/core/conversations/ingest/materialize_email_attachments.test.ts similarity index 100% rename from services/platform/convex/conversations/ingest/materialize_email_attachments.test.ts rename to services/platform/backend/core/conversations/ingest/materialize_email_attachments.test.ts diff --git a/services/platform/convex/conversations/ingest/materialize_email_attachments.ts b/services/platform/backend/core/conversations/ingest/materialize_email_attachments.ts similarity index 98% rename from services/platform/convex/conversations/ingest/materialize_email_attachments.ts rename to services/platform/backend/core/conversations/ingest/materialize_email_attachments.ts index 54293abbef..a0136545a6 100644 --- a/services/platform/convex/conversations/ingest/materialize_email_attachments.ts +++ b/services/platform/backend/core/conversations/ingest/materialize_email_attachments.ts @@ -13,7 +13,7 @@ * `sync_mailbox` host calls this. */ -import { isRecord } from '../../../lib/utils/type-utils'; +import { isRecord } from '../../../../lib/utils/type-utils'; import type { ActionCtx } from '../../lib/ctx'; import { internal } from '../../lib/handler_names'; import { diff --git a/services/platform/convex/conversations/ingest/normalize_email.test.ts b/services/platform/backend/core/conversations/ingest/normalize_email.test.ts similarity index 100% rename from services/platform/convex/conversations/ingest/normalize_email.test.ts rename to services/platform/backend/core/conversations/ingest/normalize_email.test.ts diff --git a/services/platform/convex/conversations/ingest/normalize_email.ts b/services/platform/backend/core/conversations/ingest/normalize_email.ts similarity index 100% rename from services/platform/convex/conversations/ingest/normalize_email.ts rename to services/platform/backend/core/conversations/ingest/normalize_email.ts diff --git a/services/platform/convex/conversations/ingest/normalize_external_message_id.test.ts b/services/platform/backend/core/conversations/ingest/normalize_external_message_id.test.ts similarity index 100% rename from services/platform/convex/conversations/ingest/normalize_external_message_id.test.ts rename to services/platform/backend/core/conversations/ingest/normalize_external_message_id.test.ts diff --git a/services/platform/convex/conversations/ingest/normalize_external_message_id.ts b/services/platform/backend/core/conversations/ingest/normalize_external_message_id.ts similarity index 100% rename from services/platform/convex/conversations/ingest/normalize_external_message_id.ts rename to services/platform/backend/core/conversations/ingest/normalize_external_message_id.ts diff --git a/services/platform/convex/conversations/ingest/parse_thread_reference_ids.test.ts b/services/platform/backend/core/conversations/ingest/parse_thread_reference_ids.test.ts similarity index 100% rename from services/platform/convex/conversations/ingest/parse_thread_reference_ids.test.ts rename to services/platform/backend/core/conversations/ingest/parse_thread_reference_ids.test.ts diff --git a/services/platform/convex/conversations/ingest/parse_thread_reference_ids.ts b/services/platform/backend/core/conversations/ingest/parse_thread_reference_ids.ts similarity index 100% rename from services/platform/convex/conversations/ingest/parse_thread_reference_ids.ts rename to services/platform/backend/core/conversations/ingest/parse_thread_reference_ids.ts diff --git a/services/platform/convex/conversations/ingest/query_latest_message_by_delivery_state.ts b/services/platform/backend/core/conversations/ingest/query_latest_message_by_delivery_state.ts similarity index 100% rename from services/platform/convex/conversations/ingest/query_latest_message_by_delivery_state.ts rename to services/platform/backend/core/conversations/ingest/query_latest_message_by_delivery_state.ts diff --git a/services/platform/convex/conversations/ingest/query_latest_outbound_message_for_sync.test.ts b/services/platform/backend/core/conversations/ingest/query_latest_outbound_message_for_sync.test.ts similarity index 100% rename from services/platform/convex/conversations/ingest/query_latest_outbound_message_for_sync.test.ts rename to services/platform/backend/core/conversations/ingest/query_latest_outbound_message_for_sync.test.ts diff --git a/services/platform/convex/conversations/ingest/query_latest_outbound_message_for_sync.ts b/services/platform/backend/core/conversations/ingest/query_latest_outbound_message_for_sync.ts similarity index 100% rename from services/platform/convex/conversations/ingest/query_latest_outbound_message_for_sync.ts rename to services/platform/backend/core/conversations/ingest/query_latest_outbound_message_for_sync.ts diff --git a/services/platform/convex/conversations/ingest/resolve_connector_account_email.ts b/services/platform/backend/core/conversations/ingest/resolve_connector_account_email.ts similarity index 96% rename from services/platform/convex/conversations/ingest/resolve_connector_account_email.ts rename to services/platform/backend/core/conversations/ingest/resolve_connector_account_email.ts index de2b40f12f..19ed174408 100644 --- a/services/platform/convex/conversations/ingest/resolve_connector_account_email.ts +++ b/services/platform/backend/core/conversations/ingest/resolve_connector_account_email.ts @@ -1,4 +1,4 @@ -import { isRecord } from '../../../lib/utils/type-utils'; +import { isRecord } from '../../../../lib/utils/type-utils'; import type { ActionCtx } from '../../lib/ctx'; import { internal } from '../../lib/handler_names'; diff --git a/services/platform/convex/conversations/ingest/resolve_contact_email.ts b/services/platform/backend/core/conversations/ingest/resolve_contact_email.ts similarity index 91% rename from services/platform/convex/conversations/ingest/resolve_contact_email.ts rename to services/platform/backend/core/conversations/ingest/resolve_contact_email.ts index 43d740f3f5..5691c493c0 100644 --- a/services/platform/convex/conversations/ingest/resolve_contact_email.ts +++ b/services/platform/backend/core/conversations/ingest/resolve_contact_email.ts @@ -1,4 +1,4 @@ -import { getString, isRecord } from '../../../lib/utils/type-utils'; +import { getString, isRecord } from '../../../../lib/utils/type-utils'; /** * Derive the contact address from conversation metadata (root email snapshot). diff --git a/services/platform/convex/conversations/ingest/resolve_email_conversation_target.test.ts b/services/platform/backend/core/conversations/ingest/resolve_email_conversation_target.test.ts similarity index 100% rename from services/platform/convex/conversations/ingest/resolve_email_conversation_target.test.ts rename to services/platform/backend/core/conversations/ingest/resolve_email_conversation_target.test.ts diff --git a/services/platform/convex/conversations/ingest/resolve_email_conversation_target.ts b/services/platform/backend/core/conversations/ingest/resolve_email_conversation_target.ts similarity index 100% rename from services/platform/convex/conversations/ingest/resolve_email_conversation_target.ts rename to services/platform/backend/core/conversations/ingest/resolve_email_conversation_target.ts diff --git a/services/platform/convex/conversations/ingest/reuse_stored_attachments.test.ts b/services/platform/backend/core/conversations/ingest/reuse_stored_attachments.test.ts similarity index 100% rename from services/platform/convex/conversations/ingest/reuse_stored_attachments.test.ts rename to services/platform/backend/core/conversations/ingest/reuse_stored_attachments.test.ts diff --git a/services/platform/convex/conversations/ingest/reuse_stored_attachments.ts b/services/platform/backend/core/conversations/ingest/reuse_stored_attachments.ts similarity index 98% rename from services/platform/convex/conversations/ingest/reuse_stored_attachments.ts rename to services/platform/backend/core/conversations/ingest/reuse_stored_attachments.ts index 946b4940f0..f0bb8576ea 100644 --- a/services/platform/convex/conversations/ingest/reuse_stored_attachments.ts +++ b/services/platform/backend/core/conversations/ingest/reuse_stored_attachments.ts @@ -24,7 +24,7 @@ * genuinely new attachment because a new message has nothing stored to reuse. */ -import { isRecord } from '../../../lib/utils/type-utils'; +import { isRecord } from '../../../../lib/utils/type-utils'; import type { ActionCtx } from '../../lib/ctx'; import { checkMessageExists } from './check_message_exists'; diff --git a/services/platform/convex/conversations/ingest/types.ts b/services/platform/backend/core/conversations/ingest/types.ts similarity index 100% rename from services/platform/convex/conversations/ingest/types.ts rename to services/platform/backend/core/conversations/ingest/types.ts diff --git a/services/platform/convex/conversations/ingest/update_message.ts b/services/platform/backend/core/conversations/ingest/update_message.ts similarity index 100% rename from services/platform/convex/conversations/ingest/update_message.ts rename to services/platform/backend/core/conversations/ingest/update_message.ts diff --git a/services/platform/convex/conversations/reply_from.test.ts b/services/platform/backend/core/conversations/reply_from.test.ts similarity index 100% rename from services/platform/convex/conversations/reply_from.test.ts rename to services/platform/backend/core/conversations/reply_from.test.ts diff --git a/services/platform/convex/conversations/reply_from.ts b/services/platform/backend/core/conversations/reply_from.ts similarity index 98% rename from services/platform/convex/conversations/reply_from.ts rename to services/platform/backend/core/conversations/reply_from.ts index 92df4b1703..8273d5bdf1 100644 --- a/services/platform/convex/conversations/reply_from.ts +++ b/services/platform/backend/core/conversations/reply_from.ts @@ -6,7 +6,7 @@ * helpers derive that address and decide when it's safe to send as it. */ -import { isRecord } from '../../lib/utils/type-utils'; +import { isRecord } from '../../../lib/utils/type-utils'; /** * Consumer / free-mail domains where every local-part is a different person. diff --git a/services/platform/convex/conversations/reply_to_conversation.ts b/services/platform/backend/core/conversations/reply_to_conversation.ts similarity index 98% rename from services/platform/convex/conversations/reply_to_conversation.ts rename to services/platform/backend/core/conversations/reply_to_conversation.ts index 747915ec27..cda8e21cc4 100644 --- a/services/platform/convex/conversations/reply_to_conversation.ts +++ b/services/platform/backend/core/conversations/reply_to_conversation.ts @@ -10,7 +10,7 @@ * to a default provider. */ -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import type { MutationCtx } from '../lib/ctx'; import type { Id } from '../lib/rows'; import { sendMessageViaConnector } from './send_message_via_connector'; diff --git a/services/platform/convex/conversations/send_input.ts b/services/platform/backend/core/conversations/send_input.ts similarity index 98% rename from services/platform/convex/conversations/send_input.ts rename to services/platform/backend/core/conversations/send_input.ts index 95783ca88f..1b4d7334cf 100644 --- a/services/platform/convex/conversations/send_input.ts +++ b/services/platform/backend/core/conversations/send_input.ts @@ -5,7 +5,7 @@ * read the provider's Message-ID back out of the send output. */ -import { isRecord } from '../../lib/utils/type-utils'; +import { isRecord } from '../../../lib/utils/type-utils'; import { sendConnectorAction } from './connector_slug'; import { normalizeExternalMessageId } from './ingest/normalize_external_message_id'; diff --git a/services/platform/convex/conversations/send_message_via_connector.ts b/services/platform/backend/core/conversations/send_message_via_connector.ts similarity index 99% rename from services/platform/convex/conversations/send_message_via_connector.ts rename to services/platform/backend/core/conversations/send_message_via_connector.ts index 544f38564f..599448e8f2 100644 --- a/services/platform/convex/conversations/send_message_via_connector.ts +++ b/services/platform/backend/core/conversations/send_message_via_connector.ts @@ -1,4 +1,4 @@ -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { emitAuditSuccess } from '../audit_logs/emit'; import type { MutationCtx } from '../lib/ctx'; import { internal } from '../lib/handler_names'; diff --git a/services/platform/convex/conversations/sync_mailbox.test.ts b/services/platform/backend/core/conversations/sync_mailbox.test.ts similarity index 99% rename from services/platform/convex/conversations/sync_mailbox.test.ts rename to services/platform/backend/core/conversations/sync_mailbox.test.ts index b53b958a53..f1124670d9 100644 --- a/services/platform/convex/conversations/sync_mailbox.test.ts +++ b/services/platform/backend/core/conversations/sync_mailbox.test.ts @@ -8,7 +8,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { isRecord } from '../../lib/utils/type-utils'; +import { isRecord } from '../../../lib/utils/type-utils'; import type { ActionCtx } from '../lib/ctx'; const { diff --git a/services/platform/convex/conversations/sync_mailbox.ts b/services/platform/backend/core/conversations/sync_mailbox.ts similarity index 99% rename from services/platform/convex/conversations/sync_mailbox.ts rename to services/platform/backend/core/conversations/sync_mailbox.ts index 2f1246701f..afb53c4b8b 100644 --- a/services/platform/convex/conversations/sync_mailbox.ts +++ b/services/platform/backend/core/conversations/sync_mailbox.ts @@ -16,8 +16,8 @@ import type { ConversationIngestResult, ConversationSyncCursor, ConversationSyncResult, -} from '../../lib/connectors/natives/platform-conversations'; -import { isRecord } from '../../lib/utils/type-utils'; +} from '../../../lib/connectors/natives/platform-conversations'; +import { isRecord } from '../../../lib/utils/type-utils'; import { looksLikeEmailAddress, withImapFromAddress, diff --git a/services/platform/convex/conversations/types.ts b/services/platform/backend/core/conversations/types.ts similarity index 100% rename from services/platform/convex/conversations/types.ts rename to services/platform/backend/core/conversations/types.ts diff --git a/services/platform/convex/deployment/auth_policy.test.ts b/services/platform/backend/core/deployment/auth_policy.test.ts similarity index 100% rename from services/platform/convex/deployment/auth_policy.test.ts rename to services/platform/backend/core/deployment/auth_policy.test.ts diff --git a/services/platform/convex/deployment/auth_policy.ts b/services/platform/backend/core/deployment/auth_policy.ts similarity index 96% rename from services/platform/convex/deployment/auth_policy.ts rename to services/platform/backend/core/deployment/auth_policy.ts index ab5574b66c..651b421905 100644 --- a/services/platform/convex/deployment/auth_policy.ts +++ b/services/platform/backend/core/deployment/auth_policy.ts @@ -8,7 +8,7 @@ * imports: only the pure ability + allowlist helpers. */ -import { defineAbilityFor } from '../../lib/permissions/ability'; +import { defineAbilityFor } from '../../../lib/permissions/ability'; import { isDeploymentEditor } from './editors'; export interface OrgMembership { diff --git a/services/platform/convex/deployment/editors.test.ts b/services/platform/backend/core/deployment/editors.test.ts similarity index 100% rename from services/platform/convex/deployment/editors.test.ts rename to services/platform/backend/core/deployment/editors.test.ts diff --git a/services/platform/convex/deployment/editors.ts b/services/platform/backend/core/deployment/editors.ts similarity index 100% rename from services/platform/convex/deployment/editors.ts rename to services/platform/backend/core/deployment/editors.ts diff --git a/services/platform/convex/deployment/file_utils.ts b/services/platform/backend/core/deployment/file_utils.ts similarity index 94% rename from services/platform/convex/deployment/file_utils.ts rename to services/platform/backend/core/deployment/file_utils.ts index 256c353934..ba82b171e7 100644 --- a/services/platform/convex/deployment/file_utils.ts +++ b/services/platform/backend/core/deployment/file_utils.ts @@ -12,15 +12,18 @@ * rag/convex/platform entrypoints AT BOOT, not hot-reloaded. */ -import { parseYamlOrThrow, stringifyYaml } from '../../lib/shared/config/yaml'; +import { + parseYamlOrThrow, + stringifyYaml, +} from '../../../lib/shared/config/yaml'; import type { DeploymentConfig, DeploymentSecrets, -} from '../../lib/shared/schemas/deployment'; +} from '../../../lib/shared/schemas/deployment'; import { deploymentConfigSchema, deploymentSecretsSchema, -} from '../../lib/shared/schemas/deployment'; +} from '../../../lib/shared/schemas/deployment'; import { getConfigRoot, safeJoinWithinDir, sha256 } from '../lib/file_io'; export { sha256 }; diff --git a/services/platform/convex/deployment/secret_io.ts b/services/platform/backend/core/deployment/secret_io.ts similarity index 99% rename from services/platform/convex/deployment/secret_io.ts rename to services/platform/backend/core/deployment/secret_io.ts index 31e99e7f51..4c5131bd44 100644 --- a/services/platform/convex/deployment/secret_io.ts +++ b/services/platform/backend/core/deployment/secret_io.ts @@ -19,7 +19,7 @@ import type { DeploymentSecretKey, DeploymentSecrets, -} from '../../lib/shared/schemas/deployment'; +} from '../../../lib/shared/schemas/deployment'; import { EncryptedFileWithoutKeyError, decryptSecretsFile } from '../lib/sops'; import { parseDeploymentSecrets } from './file_utils'; diff --git a/services/platform/convex/deployment/test_datastore_connection.ts b/services/platform/backend/core/deployment/test_datastore_connection.ts similarity index 100% rename from services/platform/convex/deployment/test_datastore_connection.ts rename to services/platform/backend/core/deployment/test_datastore_connection.ts diff --git a/services/platform/convex/documents/access.test.ts b/services/platform/backend/core/documents/access.test.ts similarity index 100% rename from services/platform/convex/documents/access.test.ts rename to services/platform/backend/core/documents/access.test.ts diff --git a/services/platform/convex/documents/access.ts b/services/platform/backend/core/documents/access.ts similarity index 99% rename from services/platform/convex/documents/access.ts rename to services/platform/backend/core/documents/access.ts index 474321362b..f243ae2a3d 100644 --- a/services/platform/convex/documents/access.ts +++ b/services/platform/backend/core/documents/access.ts @@ -16,7 +16,7 @@ * or `canReadDocument` (async, resolves project access for single-doc reads). */ -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import type { MutationCtx, QueryCtx } from '../lib/ctx'; import { getUserTeamIds } from '../lib/get_user_teams'; import type { Doc } from '../lib/rows'; diff --git a/services/platform/convex/documents/attest_document_bytes.test.ts b/services/platform/backend/core/documents/attest_document_bytes.test.ts similarity index 100% rename from services/platform/convex/documents/attest_document_bytes.test.ts rename to services/platform/backend/core/documents/attest_document_bytes.test.ts diff --git a/services/platform/convex/documents/attest_document_bytes.ts b/services/platform/backend/core/documents/attest_document_bytes.ts similarity index 99% rename from services/platform/convex/documents/attest_document_bytes.ts rename to services/platform/backend/core/documents/attest_document_bytes.ts index 6c175de69f..7ebafccb8a 100644 --- a/services/platform/convex/documents/attest_document_bytes.ts +++ b/services/platform/backend/core/documents/attest_document_bytes.ts @@ -2,7 +2,7 @@ import { fileTypeFromBuffer } from 'file-type'; -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { extractExtension } from './extract_extension'; const DETECTED_DOCUMENT_MIME_BY_EXTENSION: Readonly> = { diff --git a/services/platform/backend/core/documents/extract_extension.ts b/services/platform/backend/core/documents/extract_extension.ts new file mode 100644 index 0000000000..04208ef042 --- /dev/null +++ b/services/platform/backend/core/documents/extract_extension.ts @@ -0,0 +1 @@ +export { extractExtension } from '../../../lib/shared/file-types'; diff --git a/services/platform/convex/documents/get_user_names_batch.ts b/services/platform/backend/core/documents/get_user_names_batch.ts similarity index 98% rename from services/platform/convex/documents/get_user_names_batch.ts rename to services/platform/backend/core/documents/get_user_names_batch.ts index 6b2643efa1..40d169f1d5 100644 --- a/services/platform/convex/documents/get_user_names_batch.ts +++ b/services/platform/backend/core/documents/get_user_names_batch.ts @@ -6,7 +6,7 @@ * Note: Uses parallel individual lookups since Better Auth adapter doesn't support IN queries. */ -import { isRecord, getString } from '../../lib/utils/type-utils'; +import { isRecord, getString } from '../../../lib/utils/type-utils'; import { isExternalOwnerId } from '../identities/external_identities'; import type { QueryCtx } from '../lib/ctx'; import { components } from '../lib/handler_names'; diff --git a/services/platform/convex/documents/parse_yaml_map.test.ts b/services/platform/backend/core/documents/parse_yaml_map.test.ts similarity index 100% rename from services/platform/convex/documents/parse_yaml_map.test.ts rename to services/platform/backend/core/documents/parse_yaml_map.test.ts diff --git a/services/platform/convex/documents/parse_yaml_map.ts b/services/platform/backend/core/documents/parse_yaml_map.ts similarity index 100% rename from services/platform/convex/documents/parse_yaml_map.ts rename to services/platform/backend/core/documents/parse_yaml_map.ts diff --git a/services/platform/convex/documents/serialize_yaml_map.test.ts b/services/platform/backend/core/documents/serialize_yaml_map.test.ts similarity index 100% rename from services/platform/convex/documents/serialize_yaml_map.test.ts rename to services/platform/backend/core/documents/serialize_yaml_map.test.ts diff --git a/services/platform/convex/documents/serialize_yaml_map.ts b/services/platform/backend/core/documents/serialize_yaml_map.ts similarity index 100% rename from services/platform/convex/documents/serialize_yaml_map.ts rename to services/platform/backend/core/documents/serialize_yaml_map.ts diff --git a/services/platform/convex/enterprise_sso/claims.test.ts b/services/platform/backend/core/enterprise_sso/claims.test.ts similarity index 100% rename from services/platform/convex/enterprise_sso/claims.test.ts rename to services/platform/backend/core/enterprise_sso/claims.test.ts diff --git a/services/platform/convex/enterprise_sso/claims.ts b/services/platform/backend/core/enterprise_sso/claims.ts similarity index 97% rename from services/platform/convex/enterprise_sso/claims.ts rename to services/platform/backend/core/enterprise_sso/claims.ts index 559680176f..f845d7efd0 100644 --- a/services/platform/convex/enterprise_sso/claims.ts +++ b/services/platform/backend/core/enterprise_sso/claims.ts @@ -7,7 +7,7 @@ * rules and claim mappings at any of these without provider-specific code. */ -import { isRecord } from '../../lib/utils/type-utils'; +import { isRecord } from '../../../lib/utils/type-utils'; /** * Resolve a dot-path (e.g. `realm_access.roles`) inside a claims object. diff --git a/services/platform/convex/enterprise_sso/config/file_store.ts b/services/platform/backend/core/enterprise_sso/config/file_store.ts similarity index 98% rename from services/platform/convex/enterprise_sso/config/file_store.ts rename to services/platform/backend/core/enterprise_sso/config/file_store.ts index f6a80eb62c..b9e233ff39 100644 --- a/services/platform/convex/enterprise_sso/config/file_store.ts +++ b/services/platform/backend/core/enterprise_sso/config/file_store.ts @@ -23,7 +23,7 @@ import path from 'node:path'; import { type SsoConnectionFile, type SsoConnectionSecrets, -} from '../../../lib/shared/schemas/enterprise_sso'; +} from '../../../../lib/shared/schemas/enterprise_sso'; import { readDomainConfigFile } from '../../lib/config_store/read_domain_file'; import { atomicWrite, diff --git a/services/platform/convex/enterprise_sso/entra_id/adapter.test.ts b/services/platform/backend/core/enterprise_sso/entra_id/adapter.test.ts similarity index 100% rename from services/platform/convex/enterprise_sso/entra_id/adapter.test.ts rename to services/platform/backend/core/enterprise_sso/entra_id/adapter.test.ts diff --git a/services/platform/convex/enterprise_sso/entra_id/adapter.ts b/services/platform/backend/core/enterprise_sso/entra_id/adapter.ts similarity index 100% rename from services/platform/convex/enterprise_sso/entra_id/adapter.ts rename to services/platform/backend/core/enterprise_sso/entra_id/adapter.ts diff --git a/services/platform/convex/enterprise_sso/entra_id/constants.test.ts b/services/platform/backend/core/enterprise_sso/entra_id/constants.test.ts similarity index 100% rename from services/platform/convex/enterprise_sso/entra_id/constants.test.ts rename to services/platform/backend/core/enterprise_sso/entra_id/constants.test.ts diff --git a/services/platform/convex/enterprise_sso/entra_id/constants.ts b/services/platform/backend/core/enterprise_sso/entra_id/constants.ts similarity index 100% rename from services/platform/convex/enterprise_sso/entra_id/constants.ts rename to services/platform/backend/core/enterprise_sso/entra_id/constants.ts diff --git a/services/platform/convex/enterprise_sso/entra_id/error_codes.ts b/services/platform/backend/core/enterprise_sso/entra_id/error_codes.ts similarity index 100% rename from services/platform/convex/enterprise_sso/entra_id/error_codes.ts rename to services/platform/backend/core/enterprise_sso/entra_id/error_codes.ts diff --git a/services/platform/convex/enterprise_sso/entra_id/role_mapping.test.ts b/services/platform/backend/core/enterprise_sso/entra_id/role_mapping.test.ts similarity index 100% rename from services/platform/convex/enterprise_sso/entra_id/role_mapping.test.ts rename to services/platform/backend/core/enterprise_sso/entra_id/role_mapping.test.ts diff --git a/services/platform/convex/enterprise_sso/entra_id/role_mapping.ts b/services/platform/backend/core/enterprise_sso/entra_id/role_mapping.ts similarity index 100% rename from services/platform/convex/enterprise_sso/entra_id/role_mapping.ts rename to services/platform/backend/core/enterprise_sso/entra_id/role_mapping.ts diff --git a/services/platform/convex/enterprise_sso/file_utils.ts b/services/platform/backend/core/enterprise_sso/file_utils.ts similarity index 96% rename from services/platform/convex/enterprise_sso/file_utils.ts rename to services/platform/backend/core/enterprise_sso/file_utils.ts index 6db6e4cb21..d59bdd6004 100644 --- a/services/platform/convex/enterprise_sso/file_utils.ts +++ b/services/platform/backend/core/enterprise_sso/file_utils.ts @@ -24,7 +24,7 @@ * writes themselves live in `config/file_actions.ts`. */ -import { stringifyYaml } from '../../lib/shared/config/yaml'; +import { stringifyYaml } from '../../../lib/shared/config/yaml'; import { SSO_CONFIG_DOMAIN, SSO_CONNECTION_KEY, @@ -32,8 +32,8 @@ import { ssoConnectionFileSchema, type SsoConnectionSecrets, ssoConnectionSecretsSchema, -} from '../../lib/shared/schemas/enterprise_sso'; -import { zodErrorMessage } from '../../lib/shared/schemas/format-error'; +} from '../../../lib/shared/schemas/enterprise_sso'; +import { zodErrorMessage } from '../../../lib/shared/schemas/format-error'; import { resolveGovernanceDir } from '../governance/file_utils'; import { safeJoinWithinDir } from '../lib/file_io'; diff --git a/services/platform/convex/enterprise_sso/find_or_create_sso_user.test.ts b/services/platform/backend/core/enterprise_sso/find_or_create_sso_user.test.ts similarity index 100% rename from services/platform/convex/enterprise_sso/find_or_create_sso_user.test.ts rename to services/platform/backend/core/enterprise_sso/find_or_create_sso_user.test.ts diff --git a/services/platform/convex/enterprise_sso/find_or_create_sso_user.ts b/services/platform/backend/core/enterprise_sso/find_or_create_sso_user.ts similarity index 99% rename from services/platform/convex/enterprise_sso/find_or_create_sso_user.ts rename to services/platform/backend/core/enterprise_sso/find_or_create_sso_user.ts index 1f177e4769..9df1bb13aa 100644 --- a/services/platform/convex/enterprise_sso/find_or_create_sso_user.ts +++ b/services/platform/backend/core/enterprise_sso/find_or_create_sso_user.ts @@ -1,4 +1,4 @@ -import { isRecord, getString } from '../../lib/utils/type-utils'; +import { isRecord, getString } from '../../../lib/utils/type-utils'; import { normalizeAuthEmail } from '../lib/auth/normalize_auth_email'; import type { MutationCtx } from '../lib/ctx'; import { components } from '../lib/handler_names'; diff --git a/services/platform/convex/enterprise_sso/generic_oidc/adapter.test.ts b/services/platform/backend/core/enterprise_sso/generic_oidc/adapter.test.ts similarity index 100% rename from services/platform/convex/enterprise_sso/generic_oidc/adapter.test.ts rename to services/platform/backend/core/enterprise_sso/generic_oidc/adapter.test.ts diff --git a/services/platform/convex/enterprise_sso/generic_oidc/adapter.ts b/services/platform/backend/core/enterprise_sso/generic_oidc/adapter.ts similarity index 99% rename from services/platform/convex/enterprise_sso/generic_oidc/adapter.ts rename to services/platform/backend/core/enterprise_sso/generic_oidc/adapter.ts index 6d1c391852..2dd9c22d81 100644 --- a/services/platform/convex/enterprise_sso/generic_oidc/adapter.ts +++ b/services/platform/backend/core/enterprise_sso/generic_oidc/adapter.ts @@ -1,6 +1,6 @@ // The role matcher is provider-agnostic (it matches jobTitle / appRole / // group / claim values against rules) — reused here rather than duplicated. -import { isRecord } from '../../../lib/utils/type-utils'; +import { isRecord } from '../../../../lib/utils/type-utils'; import { claimValueToStrings, resolveClaimPath } from '../claims'; import { mapEntraRoleToPlatformRole } from '../entra_id/role_mapping'; import { discoverOidc, OIDC_FETCH_TIMEOUT_MS } from '../oidc_discovery'; diff --git a/services/platform/convex/enterprise_sso/login/authorize_handler.test.ts b/services/platform/backend/core/enterprise_sso/login/authorize_handler.test.ts similarity index 100% rename from services/platform/convex/enterprise_sso/login/authorize_handler.test.ts rename to services/platform/backend/core/enterprise_sso/login/authorize_handler.test.ts diff --git a/services/platform/convex/enterprise_sso/login/authorize_handler.ts b/services/platform/backend/core/enterprise_sso/login/authorize_handler.ts similarity index 100% rename from services/platform/convex/enterprise_sso/login/authorize_handler.ts rename to services/platform/backend/core/enterprise_sso/login/authorize_handler.ts diff --git a/services/platform/convex/enterprise_sso/login/callback_handler.test.ts b/services/platform/backend/core/enterprise_sso/login/callback_handler.test.ts similarity index 100% rename from services/platform/convex/enterprise_sso/login/callback_handler.test.ts rename to services/platform/backend/core/enterprise_sso/login/callback_handler.test.ts diff --git a/services/platform/convex/enterprise_sso/login/callback_handler.ts b/services/platform/backend/core/enterprise_sso/login/callback_handler.ts similarity index 100% rename from services/platform/convex/enterprise_sso/login/callback_handler.ts rename to services/platform/backend/core/enterprise_sso/login/callback_handler.ts diff --git a/services/platform/convex/enterprise_sso/login/discover_handler.ts b/services/platform/backend/core/enterprise_sso/login/discover_handler.ts similarity index 100% rename from services/platform/convex/enterprise_sso/login/discover_handler.ts rename to services/platform/backend/core/enterprise_sso/login/discover_handler.ts diff --git a/services/platform/convex/enterprise_sso/login/finish_login.ts b/services/platform/backend/core/enterprise_sso/login/finish_login.ts similarity index 100% rename from services/platform/convex/enterprise_sso/login/finish_login.ts rename to services/platform/backend/core/enterprise_sso/login/finish_login.ts diff --git a/services/platform/convex/enterprise_sso/login/login_audit.ts b/services/platform/backend/core/enterprise_sso/login/login_audit.ts similarity index 100% rename from services/platform/convex/enterprise_sso/login/login_audit.ts rename to services/platform/backend/core/enterprise_sso/login/login_audit.ts diff --git a/services/platform/convex/enterprise_sso/login/redirect_with_error.ts b/services/platform/backend/core/enterprise_sso/login/redirect_with_error.ts similarity index 100% rename from services/platform/convex/enterprise_sso/login/redirect_with_error.ts rename to services/platform/backend/core/enterprise_sso/login/redirect_with_error.ts diff --git a/services/platform/convex/enterprise_sso/oauth2/adapter.ts b/services/platform/backend/core/enterprise_sso/oauth2/adapter.ts similarity index 98% rename from services/platform/convex/enterprise_sso/oauth2/adapter.ts rename to services/platform/backend/core/enterprise_sso/oauth2/adapter.ts index 341cb8b8b2..95a3752737 100644 --- a/services/platform/convex/enterprise_sso/oauth2/adapter.ts +++ b/services/platform/backend/core/enterprise_sso/oauth2/adapter.ts @@ -5,7 +5,7 @@ * not a `.well-known` discovery document. */ -import { isRecord } from '../../../lib/utils/type-utils'; +import { isRecord } from '../../../../lib/utils/type-utils'; import { claimValueToStrings, resolveClaimPath } from '../claims'; import { mapEntraRoleToPlatformRole } from '../entra_id/role_mapping'; import { OIDC_FETCH_TIMEOUT_MS } from '../oidc_discovery'; diff --git a/services/platform/convex/enterprise_sso/oidc_discovery.ts b/services/platform/backend/core/enterprise_sso/oidc_discovery.ts similarity index 98% rename from services/platform/convex/enterprise_sso/oidc_discovery.ts rename to services/platform/backend/core/enterprise_sso/oidc_discovery.ts index d70a48fd05..899f79cc5e 100644 --- a/services/platform/convex/enterprise_sso/oidc_discovery.ts +++ b/services/platform/backend/core/enterprise_sso/oidc_discovery.ts @@ -9,7 +9,7 @@ * infrequent and the document is cacheable), so no endpoint is persisted. */ -import { isRecord } from '../../lib/utils/type-utils'; +import { isRecord } from '../../../lib/utils/type-utils'; export interface OidcEndpoints { issuer: string; diff --git a/services/platform/convex/enterprise_sso/pkce.test.ts b/services/platform/backend/core/enterprise_sso/pkce.test.ts similarity index 100% rename from services/platform/convex/enterprise_sso/pkce.test.ts rename to services/platform/backend/core/enterprise_sso/pkce.test.ts diff --git a/services/platform/convex/enterprise_sso/pkce.ts b/services/platform/backend/core/enterprise_sso/pkce.ts similarity index 100% rename from services/platform/convex/enterprise_sso/pkce.ts rename to services/platform/backend/core/enterprise_sso/pkce.ts diff --git a/services/platform/convex/enterprise_sso/registry.ts b/services/platform/backend/core/enterprise_sso/registry.ts similarity index 100% rename from services/platform/convex/enterprise_sso/registry.ts rename to services/platform/backend/core/enterprise_sso/registry.ts diff --git a/services/platform/convex/enterprise_sso/saml/acs_handler.ts b/services/platform/backend/core/enterprise_sso/saml/acs_handler.ts similarity index 100% rename from services/platform/convex/enterprise_sso/saml/acs_handler.ts rename to services/platform/backend/core/enterprise_sso/saml/acs_handler.ts diff --git a/services/platform/convex/enterprise_sso/saml/attributes.test.ts b/services/platform/backend/core/enterprise_sso/saml/attributes.test.ts similarity index 100% rename from services/platform/convex/enterprise_sso/saml/attributes.test.ts rename to services/platform/backend/core/enterprise_sso/saml/attributes.test.ts diff --git a/services/platform/convex/enterprise_sso/saml/attributes.ts b/services/platform/backend/core/enterprise_sso/saml/attributes.ts similarity index 100% rename from services/platform/convex/enterprise_sso/saml/attributes.ts rename to services/platform/backend/core/enterprise_sso/saml/attributes.ts diff --git a/services/platform/convex/enterprise_sso/saml/login_handler.ts b/services/platform/backend/core/enterprise_sso/saml/login_handler.ts similarity index 100% rename from services/platform/convex/enterprise_sso/saml/login_handler.ts rename to services/platform/backend/core/enterprise_sso/saml/login_handler.ts diff --git a/services/platform/convex/enterprise_sso/saml/metadata_handler.ts b/services/platform/backend/core/enterprise_sso/saml/metadata_handler.ts similarity index 100% rename from services/platform/convex/enterprise_sso/saml/metadata_handler.ts rename to services/platform/backend/core/enterprise_sso/saml/metadata_handler.ts diff --git a/services/platform/convex/enterprise_sso/saml/parse_metadata.test.ts b/services/platform/backend/core/enterprise_sso/saml/parse_metadata.test.ts similarity index 100% rename from services/platform/convex/enterprise_sso/saml/parse_metadata.test.ts rename to services/platform/backend/core/enterprise_sso/saml/parse_metadata.test.ts diff --git a/services/platform/convex/enterprise_sso/saml/parse_metadata.ts b/services/platform/backend/core/enterprise_sso/saml/parse_metadata.ts similarity index 98% rename from services/platform/convex/enterprise_sso/saml/parse_metadata.ts rename to services/platform/backend/core/enterprise_sso/saml/parse_metadata.ts index 28699496ad..9fa11b195b 100644 --- a/services/platform/convex/enterprise_sso/saml/parse_metadata.ts +++ b/services/platform/backend/core/enterprise_sso/saml/parse_metadata.ts @@ -1,7 +1,7 @@ 'use node'; import { XMLParser } from 'fast-xml-parser'; -import { safeFetch, SafeFetchError } from '../../lib/http/safe_fetch'; +import { safeFetch, SafeFetchError } from '../../../../lib/net/safe-fetch'; /** * SAML 2.0 federation-metadata ingestion (issue #2652). Every IdP (Entra, diff --git a/services/platform/convex/enterprise_sso/saml/validate_assertion.ts b/services/platform/backend/core/enterprise_sso/saml/validate_assertion.ts similarity index 100% rename from services/platform/convex/enterprise_sso/saml/validate_assertion.ts rename to services/platform/backend/core/enterprise_sso/saml/validate_assertion.ts diff --git a/services/platform/convex/enterprise_sso/sign_cookie_value.ts b/services/platform/backend/core/enterprise_sso/sign_cookie_value.ts similarity index 100% rename from services/platform/convex/enterprise_sso/sign_cookie_value.ts rename to services/platform/backend/core/enterprise_sso/sign_cookie_value.ts diff --git a/services/platform/convex/enterprise_sso/types.ts b/services/platform/backend/core/enterprise_sso/types.ts similarity index 95% rename from services/platform/convex/enterprise_sso/types.ts rename to services/platform/backend/core/enterprise_sso/types.ts index 9ae92a4b24..1ed28a6db1 100644 --- a/services/platform/convex/enterprise_sso/types.ts +++ b/services/platform/backend/core/enterprise_sso/types.ts @@ -17,7 +17,7 @@ export type { SsoProviderCapabilities, SsoAuthContext, AttributeMapping, -} from '../../lib/shared/schemas/enterprise_sso'; +} from '../../../lib/shared/schemas/enterprise_sso'; import type { PlatformRole, @@ -26,7 +26,7 @@ import type { SsoProviderCapabilities, SsoTokens, SsoUserInfo, -} from '../../lib/shared/schemas/enterprise_sso'; +} from '../../../lib/shared/schemas/enterprise_sso'; /** Resolved (decrypted) sign-in config handed to an OIDC/OAuth2 adapter. */ export interface SsoProviderConfig { diff --git a/services/platform/convex/events/emit.ts b/services/platform/backend/core/events/emit.ts similarity index 100% rename from services/platform/convex/events/emit.ts rename to services/platform/backend/core/events/emit.ts diff --git a/services/platform/convex/feedback/stats.test.ts b/services/platform/backend/core/feedback/stats.test.ts similarity index 100% rename from services/platform/convex/feedback/stats.test.ts rename to services/platform/backend/core/feedback/stats.test.ts diff --git a/services/platform/convex/feedback/stats.ts b/services/platform/backend/core/feedback/stats.ts similarity index 100% rename from services/platform/convex/feedback/stats.ts rename to services/platform/backend/core/feedback/stats.ts diff --git a/services/platform/convex/file_metadata/audio_preprocess.ts b/services/platform/backend/core/file_metadata/audio_preprocess.ts similarity index 100% rename from services/platform/convex/file_metadata/audio_preprocess.ts rename to services/platform/backend/core/file_metadata/audio_preprocess.ts diff --git a/services/platform/convex/file_metadata/paragraphize.ts b/services/platform/backend/core/file_metadata/paragraphize.ts similarity index 100% rename from services/platform/convex/file_metadata/paragraphize.ts rename to services/platform/backend/core/file_metadata/paragraphize.ts diff --git a/services/platform/convex/file_metadata/source_from_provider.test.ts b/services/platform/backend/core/file_metadata/source_from_provider.test.ts similarity index 100% rename from services/platform/convex/file_metadata/source_from_provider.test.ts rename to services/platform/backend/core/file_metadata/source_from_provider.test.ts diff --git a/services/platform/convex/file_metadata/source_from_provider.ts b/services/platform/backend/core/file_metadata/source_from_provider.ts similarity index 100% rename from services/platform/convex/file_metadata/source_from_provider.ts rename to services/platform/backend/core/file_metadata/source_from_provider.ts diff --git a/services/platform/convex/file_metadata/transcribe_audio.ts b/services/platform/backend/core/file_metadata/transcribe_audio.ts similarity index 99% rename from services/platform/convex/file_metadata/transcribe_audio.ts rename to services/platform/backend/core/file_metadata/transcribe_audio.ts index ae063535f8..6bbef86d41 100644 --- a/services/platform/convex/file_metadata/transcribe_audio.ts +++ b/services/platform/backend/core/file_metadata/transcribe_audio.ts @@ -1,11 +1,11 @@ 'use node'; -import { TRANSCRIPTION_SLUG } from '../../lib/shared/constants/usage'; +import { checkProviderHostPolicy } from '../../../lib/net/host-policy'; +import { TRANSCRIPTION_SLUG } from '../../../lib/shared/constants/usage'; import { estimateTranscriptionCostCents } from '../governance/cost_estimation'; import type { ActionCtx } from '../lib/ctx'; import { classifyTranscriptionError } from '../lib/errors/classify_transcription_error'; import { internal } from '../lib/handler_names'; import { orgSlugFromIdOrNull } from '../lib/helpers/org_slug'; -import { checkProviderHostPolicy } from '../lib/http/host_policy'; import { resolveTranscriptionModel } from '../lib/providers/resolve_transcription_model'; import { readBlobBytes } from '../lib/storage/blob_access'; import { convexStorageId, type BlobRef } from '../lib/storage/blob_ref'; diff --git a/services/platform/convex/file_metadata/transcribe_dictation.ts b/services/platform/backend/core/file_metadata/transcribe_dictation.ts similarity index 100% rename from services/platform/convex/file_metadata/transcribe_dictation.ts rename to services/platform/backend/core/file_metadata/transcribe_dictation.ts diff --git a/services/platform/convex/file_metadata/transcription_request.test.ts b/services/platform/backend/core/file_metadata/transcription_request.test.ts similarity index 100% rename from services/platform/convex/file_metadata/transcription_request.test.ts rename to services/platform/backend/core/file_metadata/transcription_request.test.ts diff --git a/services/platform/convex/file_metadata/transcription_request.ts b/services/platform/backend/core/file_metadata/transcription_request.ts similarity index 100% rename from services/platform/convex/file_metadata/transcription_request.ts rename to services/platform/backend/core/file_metadata/transcription_request.ts diff --git a/services/platform/convex/google_drive/derive_sync_targets.test.ts b/services/platform/backend/core/google_drive/derive_sync_targets.test.ts similarity index 100% rename from services/platform/convex/google_drive/derive_sync_targets.test.ts rename to services/platform/backend/core/google_drive/derive_sync_targets.test.ts diff --git a/services/platform/convex/google_drive/derive_sync_targets.ts b/services/platform/backend/core/google_drive/derive_sync_targets.ts similarity index 100% rename from services/platform/convex/google_drive/derive_sync_targets.ts rename to services/platform/backend/core/google_drive/derive_sync_targets.ts diff --git a/services/platform/convex/google_drive/get_file_metadata.ts b/services/platform/backend/core/google_drive/get_file_metadata.ts similarity index 96% rename from services/platform/convex/google_drive/get_file_metadata.ts rename to services/platform/backend/core/google_drive/get_file_metadata.ts index 985e599b31..0803c23aba 100644 --- a/services/platform/convex/google_drive/get_file_metadata.ts +++ b/services/platform/backend/core/google_drive/get_file_metadata.ts @@ -1,4 +1,4 @@ -import { fetchJson } from '../../lib/utils/type-utils'; +import { fetchJson } from '../../../lib/utils/type-utils'; import { isGoogleWorkspaceMime } from './list_files'; export interface FileMetadataResult { diff --git a/services/platform/convex/google_drive/import_files.ts b/services/platform/backend/core/google_drive/import_files.ts similarity index 99% rename from services/platform/convex/google_drive/import_files.ts rename to services/platform/backend/core/google_drive/import_files.ts index 6868c7925d..be7b0b9404 100644 --- a/services/platform/convex/google_drive/import_files.ts +++ b/services/platform/backend/core/google_drive/import_files.ts @@ -2,7 +2,7 @@ * Import Google Drive files into Knowledge Documents (one-time or sync). */ -import { resolveFileType } from '../../lib/shared/file-types'; +import { resolveFileType } from '../../../lib/shared/file-types'; import type { Id } from '../lib/rows'; import type { BlobRef } from '../lib/storage/blob_ref'; import { deriveSyncTargets, type SyncTarget } from './derive_sync_targets'; diff --git a/services/platform/convex/google_drive/list_files.test.ts b/services/platform/backend/core/google_drive/list_files.test.ts similarity index 100% rename from services/platform/convex/google_drive/list_files.test.ts rename to services/platform/backend/core/google_drive/list_files.test.ts diff --git a/services/platform/convex/google_drive/list_files.ts b/services/platform/backend/core/google_drive/list_files.ts similarity index 98% rename from services/platform/convex/google_drive/list_files.ts rename to services/platform/backend/core/google_drive/list_files.ts index 45c2667c4f..de603975d3 100644 --- a/services/platform/convex/google_drive/list_files.ts +++ b/services/platform/backend/core/google_drive/list_files.ts @@ -1,4 +1,4 @@ -import { fetchJson } from '../../lib/utils/type-utils'; +import { fetchJson } from '../../../lib/utils/type-utils'; export const GOOGLE_FOLDER_MIME = 'application/vnd.google-apps.folder'; diff --git a/services/platform/convex/google_drive/list_folder_contents.ts b/services/platform/backend/core/google_drive/list_folder_contents.ts similarity index 98% rename from services/platform/convex/google_drive/list_folder_contents.ts rename to services/platform/backend/core/google_drive/list_folder_contents.ts index b73c7d1c2a..0f8d88106a 100644 --- a/services/platform/convex/google_drive/list_folder_contents.ts +++ b/services/platform/backend/core/google_drive/list_folder_contents.ts @@ -2,7 +2,7 @@ * List Google Drive folder contents for sync reconcile. */ -import { fetchJson } from '../../lib/utils/type-utils'; +import { fetchJson } from '../../../lib/utils/type-utils'; import { GOOGLE_FOLDER_MIME, isGoogleWorkspaceMime } from './list_files'; export interface FileItem { diff --git a/services/platform/convex/governance/budget_enforcement.test.ts b/services/platform/backend/core/governance/budget_enforcement.test.ts similarity index 99% rename from services/platform/convex/governance/budget_enforcement.test.ts rename to services/platform/backend/core/governance/budget_enforcement.test.ts index e4822f8a85..64126b4d54 100644 --- a/services/platform/convex/governance/budget_enforcement.test.ts +++ b/services/platform/backend/core/governance/budget_enforcement.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { BudgetRule } from '../../lib/shared/schemas/governance'; +import type { BudgetRule } from '../../../lib/shared/schemas/governance'; import { checkRuleAgainstUsage, collectAllApplicableRules, diff --git a/services/platform/convex/governance/budget_enforcement.ts b/services/platform/backend/core/governance/budget_enforcement.ts similarity index 99% rename from services/platform/convex/governance/budget_enforcement.ts rename to services/platform/backend/core/governance/budget_enforcement.ts index b47686b8a1..edb38f9a80 100644 --- a/services/platform/convex/governance/budget_enforcement.ts +++ b/services/platform/backend/core/governance/budget_enforcement.ts @@ -1,7 +1,7 @@ import type { BudgetConfig, BudgetRule, -} from '../../lib/shared/schemas/governance'; +} from '../../../lib/shared/schemas/governance'; import type { QueryCtx } from '../lib/ctx'; import { buildPeriodKey, readPolicyConfig } from './helpers'; diff --git a/services/platform/convex/governance/competence.ts b/services/platform/backend/core/governance/competence.ts similarity index 100% rename from services/platform/convex/governance/competence.ts rename to services/platform/backend/core/governance/competence.ts diff --git a/services/platform/convex/governance/cost_estimation.ts b/services/platform/backend/core/governance/cost_estimation.ts similarity index 100% rename from services/platform/convex/governance/cost_estimation.ts rename to services/platform/backend/core/governance/cost_estimation.ts diff --git a/services/platform/convex/governance/dsar_policy.test.ts b/services/platform/backend/core/governance/dsar_policy.test.ts similarity index 100% rename from services/platform/convex/governance/dsar_policy.test.ts rename to services/platform/backend/core/governance/dsar_policy.test.ts diff --git a/services/platform/convex/governance/dsar_policy.ts b/services/platform/backend/core/governance/dsar_policy.ts similarity index 97% rename from services/platform/convex/governance/dsar_policy.ts rename to services/platform/backend/core/governance/dsar_policy.ts index b1294bdb94..ec5143de92 100644 --- a/services/platform/convex/governance/dsar_policy.ts +++ b/services/platform/backend/core/governance/dsar_policy.ts @@ -2,7 +2,7 @@ import { DEFAULT_DSAR_GOVERNANCE, type DsarGovernanceConfig, dsarGovernanceConfigSchema, -} from '../../lib/shared/schemas/governance'; +} from '../../../lib/shared/schemas/governance'; import { readConfigCacheRow } from '../lib/config_cache/read'; import type { QueryCtx } from '../lib/ctx'; diff --git a/services/platform/convex/governance/erasure_constants.ts b/services/platform/backend/core/governance/erasure_constants.ts similarity index 100% rename from services/platform/convex/governance/erasure_constants.ts rename to services/platform/backend/core/governance/erasure_constants.ts diff --git a/services/platform/convex/governance/feature_enforcement.test.ts b/services/platform/backend/core/governance/feature_enforcement.test.ts similarity index 100% rename from services/platform/convex/governance/feature_enforcement.test.ts rename to services/platform/backend/core/governance/feature_enforcement.test.ts diff --git a/services/platform/convex/governance/feature_enforcement.ts b/services/platform/backend/core/governance/feature_enforcement.ts similarity index 98% rename from services/platform/convex/governance/feature_enforcement.ts rename to services/platform/backend/core/governance/feature_enforcement.ts index f7b189f57c..c6f85d97ce 100644 --- a/services/platform/convex/governance/feature_enforcement.ts +++ b/services/platform/backend/core/governance/feature_enforcement.ts @@ -1,7 +1,7 @@ import type { FeatureFlagsConfig, FeatureFlagRule, -} from '../../lib/shared/schemas/governance'; +} from '../../../lib/shared/schemas/governance'; import type { QueryCtx } from '../lib/ctx'; import { readPolicyConfig } from './helpers'; diff --git a/services/platform/convex/governance/file_utils.ts b/services/platform/backend/core/governance/file_utils.ts similarity index 97% rename from services/platform/convex/governance/file_utils.ts rename to services/platform/backend/core/governance/file_utils.ts index 8f8d5e37a5..3d7f495912 100644 --- a/services/platform/convex/governance/file_utils.ts +++ b/services/platform/backend/core/governance/file_utils.ts @@ -29,19 +29,19 @@ import path from 'node:path'; -import { stringifyYaml } from '../../lib/shared/config/yaml'; -import { zodErrorMessage } from '../../lib/shared/schemas/format-error'; +import { stringifyYaml } from '../../../lib/shared/config/yaml'; +import { zodErrorMessage } from '../../../lib/shared/schemas/format-error'; import { fileBaseToPolicyType, isFilePolicyType, POLICY_SCHEMAS, policyTypeToFileBase, type FilePolicyType, -} from '../../lib/shared/schemas/governance'; +} from '../../../lib/shared/schemas/governance'; import { retentionDefaultsConfigSchema, type RetentionDefaultsConfig, -} from '../../lib/shared/schemas/retention'; +} from '../../../lib/shared/schemas/retention'; import { getConfigRoot, safeJoinWithinDir, diff --git a/services/platform/convex/governance/get_org_usage_metrics.ts b/services/platform/backend/core/governance/get_org_usage_metrics.ts similarity index 99% rename from services/platform/convex/governance/get_org_usage_metrics.ts rename to services/platform/backend/core/governance/get_org_usage_metrics.ts index 37e6ba0138..51f0e7ccca 100644 --- a/services/platform/convex/governance/get_org_usage_metrics.ts +++ b/services/platform/backend/core/governance/get_org_usage_metrics.ts @@ -1,7 +1,7 @@ import { bucketAgentSlug, classifyUsageRow, -} from '../../lib/shared/constants/usage'; +} from '../../../lib/shared/constants/usage'; import { getUserNamesBatch } from '../documents/get_user_names_batch'; import type { QueryCtx } from '../lib/ctx'; import { buildPeriodKeyFromTimestamp } from './helpers'; diff --git a/services/platform/convex/governance/helpers.test.ts b/services/platform/backend/core/governance/helpers.test.ts similarity index 100% rename from services/platform/convex/governance/helpers.test.ts rename to services/platform/backend/core/governance/helpers.test.ts diff --git a/services/platform/convex/governance/helpers.ts b/services/platform/backend/core/governance/helpers.ts similarity index 98% rename from services/platform/convex/governance/helpers.ts rename to services/platform/backend/core/governance/helpers.ts index 6fdba18313..a7ba893a45 100644 --- a/services/platform/convex/governance/helpers.ts +++ b/services/platform/backend/core/governance/helpers.ts @@ -7,8 +7,8 @@ import { type PolicyType, type TwoFactorPolicyConfig, twoFactorPolicyConfigSchema, -} from '../../lib/shared/schemas/governance'; -import { isRecord } from '../../lib/utils/type-utils'; +} from '../../../lib/shared/schemas/governance'; +import { isRecord } from '../../../lib/utils/type-utils'; import { readConfigCacheRow } from '../lib/config_cache/read'; import type { DatabaseReader, QueryCtx } from '../lib/ctx'; diff --git a/services/platform/convex/governance/model_access_enforcement.test.ts b/services/platform/backend/core/governance/model_access_enforcement.test.ts similarity index 98% rename from services/platform/convex/governance/model_access_enforcement.test.ts rename to services/platform/backend/core/governance/model_access_enforcement.test.ts index 4bc0bd25e1..cb1b29d09e 100644 --- a/services/platform/convex/governance/model_access_enforcement.test.ts +++ b/services/platform/backend/core/governance/model_access_enforcement.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { ModelAccessConfig } from '../../lib/shared/schemas/governance'; +import type { ModelAccessConfig } from '../../../lib/shared/schemas/governance'; import { _testInternals } from './model_access_enforcement'; const { resolveAllowedAndBlockedModels, isModelPermitted } = _testInternals; diff --git a/services/platform/convex/governance/model_access_enforcement.ts b/services/platform/backend/core/governance/model_access_enforcement.ts similarity index 97% rename from services/platform/convex/governance/model_access_enforcement.ts rename to services/platform/backend/core/governance/model_access_enforcement.ts index 4ee52ada64..256e69217a 100644 --- a/services/platform/convex/governance/model_access_enforcement.ts +++ b/services/platform/backend/core/governance/model_access_enforcement.ts @@ -1,5 +1,5 @@ -import type { ModelAccessConfig } from '../../lib/shared/schemas/governance'; -import { stripModelRefQualifier } from '../../lib/shared/utils/model-ref'; +import type { ModelAccessConfig } from '../../../lib/shared/schemas/governance'; +import { stripModelRefQualifier } from '../../../lib/shared/utils/model-ref'; import type { QueryCtx } from '../lib/ctx'; import { readPolicyConfig } from './helpers'; diff --git a/services/platform/convex/governance/resolve_default_model.test.ts b/services/platform/backend/core/governance/resolve_default_model.test.ts similarity index 100% rename from services/platform/convex/governance/resolve_default_model.test.ts rename to services/platform/backend/core/governance/resolve_default_model.test.ts diff --git a/services/platform/convex/governance/resolve_default_model.ts b/services/platform/backend/core/governance/resolve_default_model.ts similarity index 97% rename from services/platform/convex/governance/resolve_default_model.ts rename to services/platform/backend/core/governance/resolve_default_model.ts index a9cad962b3..51ba06ece9 100644 --- a/services/platform/convex/governance/resolve_default_model.ts +++ b/services/platform/backend/core/governance/resolve_default_model.ts @@ -1,7 +1,7 @@ import type { DefaultModelsConfig, DefaultModelRule, -} from '../../lib/shared/schemas/governance'; +} from '../../../lib/shared/schemas/governance'; import type { QueryCtx } from '../lib/ctx'; import { readPolicyConfig } from './helpers'; import { checkModelAccess } from './model_access_enforcement'; diff --git a/services/platform/convex/governance/retention_bounds_proposal.test.ts b/services/platform/backend/core/governance/retention_bounds_proposal.test.ts similarity index 100% rename from services/platform/convex/governance/retention_bounds_proposal.test.ts rename to services/platform/backend/core/governance/retention_bounds_proposal.test.ts diff --git a/services/platform/convex/governance/retention_bounds_proposal.ts b/services/platform/backend/core/governance/retention_bounds_proposal.ts similarity index 97% rename from services/platform/convex/governance/retention_bounds_proposal.ts rename to services/platform/backend/core/governance/retention_bounds_proposal.ts index 1381f22309..986a946f38 100644 --- a/services/platform/convex/governance/retention_bounds_proposal.ts +++ b/services/platform/backend/core/governance/retention_bounds_proposal.ts @@ -2,8 +2,8 @@ import { RETENTION_CATEGORIES, type AppliedBoundsByCategory, type RetentionCategory, -} from '../../lib/shared/schemas/retention'; -import { isRecord } from '../../lib/utils/type-utils'; +} from '../../../lib/shared/schemas/retention'; +import { isRecord } from '../../../lib/utils/type-utils'; const POLICY_FIELD_BY_CATEGORY: Record = { documents: 'documentsRetentionDays', userTempHours: 'userTempRetentionHours', diff --git a/services/platform/convex/governance/retention_floors.test.ts b/services/platform/backend/core/governance/retention_floors.test.ts similarity index 99% rename from services/platform/convex/governance/retention_floors.test.ts rename to services/platform/backend/core/governance/retention_floors.test.ts index 16734741b6..9253363d1f 100644 --- a/services/platform/convex/governance/retention_floors.test.ts +++ b/services/platform/backend/core/governance/retention_floors.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import type { RetentionDefaultsConfig } from '../../lib/shared/schemas/retention'; +import type { RetentionDefaultsConfig } from '../../../lib/shared/schemas/retention'; import { RetentionBoundsViolation, RetentionConfigMissingError, diff --git a/services/platform/convex/governance/retention_floors.ts b/services/platform/backend/core/governance/retention_floors.ts similarity index 99% rename from services/platform/convex/governance/retention_floors.ts rename to services/platform/backend/core/governance/retention_floors.ts index 98745c6765..137c88b6f6 100644 --- a/services/platform/convex/governance/retention_floors.ts +++ b/services/platform/backend/core/governance/retention_floors.ts @@ -44,7 +44,7 @@ import { RETENTION_CATEGORIES, type RetentionCategory, type RetentionDefaultsConfig, -} from '../../lib/shared/schemas/retention'; +} from '../../../lib/shared/schemas/retention'; /** * Per-binding env-resolution detail. Captured per `min` / `max` / diff --git a/services/platform/convex/governance/review_policy.ts b/services/platform/backend/core/governance/review_policy.ts similarity index 97% rename from services/platform/convex/governance/review_policy.ts rename to services/platform/backend/core/governance/review_policy.ts index 09325b0cb9..74f0611d5d 100644 --- a/services/platform/convex/governance/review_policy.ts +++ b/services/platform/backend/core/governance/review_policy.ts @@ -19,7 +19,7 @@ import { type ReviewPolicyConfig, reviewPolicyConfigSchema, -} from '../../lib/shared/schemas/governance'; +} from '../../../lib/shared/schemas/governance'; import type { DatabaseReader } from '../lib/ctx'; import { readPolicyRow } from './helpers'; diff --git a/services/platform/convex/governance/schema.ts b/services/platform/backend/core/governance/schema.ts similarity index 100% rename from services/platform/convex/governance/schema.ts rename to services/platform/backend/core/governance/schema.ts diff --git a/services/platform/convex/governance/session_idle_enforcement.ts b/services/platform/backend/core/governance/session_idle_enforcement.ts similarity index 91% rename from services/platform/convex/governance/session_idle_enforcement.ts rename to services/platform/backend/core/governance/session_idle_enforcement.ts index bb7cb91441..0904e4d817 100644 --- a/services/platform/convex/governance/session_idle_enforcement.ts +++ b/services/platform/backend/core/governance/session_idle_enforcement.ts @@ -1,5 +1,5 @@ -import { sessionIdleTimeoutConfigSchema } from '../../lib/shared/schemas/governance'; -import { resolveEffectiveIdleMinutes } from '../../lib/shared/session-idle'; +import { sessionIdleTimeoutConfigSchema } from '../../../lib/shared/schemas/governance'; +import { resolveEffectiveIdleMinutes } from '../../../lib/shared/session-idle'; interface OrgIdleWindow { organizationId: string; minutes: number; diff --git a/services/platform/convex/governance/soft_delete.ts b/services/platform/backend/core/governance/soft_delete.ts similarity index 100% rename from services/platform/convex/governance/soft_delete.ts rename to services/platform/backend/core/governance/soft_delete.ts diff --git a/services/platform/convex/http_connectors/authorize_url.ts b/services/platform/backend/core/http_connectors/authorize_url.ts similarity index 100% rename from services/platform/convex/http_connectors/authorize_url.ts rename to services/platform/backend/core/http_connectors/authorize_url.ts diff --git a/services/platform/convex/http_connectors/deployment_config.ts b/services/platform/backend/core/http_connectors/deployment_config.ts similarity index 100% rename from services/platform/convex/http_connectors/deployment_config.ts rename to services/platform/backend/core/http_connectors/deployment_config.ts diff --git a/services/platform/convex/http_connectors/error_page.ts b/services/platform/backend/core/http_connectors/error_page.ts similarity index 100% rename from services/platform/convex/http_connectors/error_page.ts rename to services/platform/backend/core/http_connectors/error_page.ts diff --git a/services/platform/convex/http_connectors/oauth_state.ts b/services/platform/backend/core/http_connectors/oauth_state.ts similarity index 100% rename from services/platform/convex/http_connectors/oauth_state.ts rename to services/platform/backend/core/http_connectors/oauth_state.ts diff --git a/services/platform/convex/http_connectors/slack_signature.test.ts b/services/platform/backend/core/http_connectors/slack_signature.test.ts similarity index 100% rename from services/platform/convex/http_connectors/slack_signature.test.ts rename to services/platform/backend/core/http_connectors/slack_signature.test.ts diff --git a/services/platform/convex/http_connectors/slack_signature.ts b/services/platform/backend/core/http_connectors/slack_signature.ts similarity index 100% rename from services/platform/convex/http_connectors/slack_signature.ts rename to services/platform/backend/core/http_connectors/slack_signature.ts diff --git a/services/platform/convex/http_connectors/token_exchange.test.ts b/services/platform/backend/core/http_connectors/token_exchange.test.ts similarity index 100% rename from services/platform/convex/http_connectors/token_exchange.test.ts rename to services/platform/backend/core/http_connectors/token_exchange.test.ts diff --git a/services/platform/convex/http_connectors/token_exchange.ts b/services/platform/backend/core/http_connectors/token_exchange.ts similarity index 98% rename from services/platform/convex/http_connectors/token_exchange.ts rename to services/platform/backend/core/http_connectors/token_exchange.ts index 73f09ef4e5..70be0c0f19 100644 --- a/services/platform/convex/http_connectors/token_exchange.ts +++ b/services/platform/backend/core/http_connectors/token_exchange.ts @@ -14,7 +14,7 @@ * injectable so the whole surface is testable without a network. */ -import { getNumber, getString, isRecord } from '../../lib/utils/type-utils'; +import { getNumber, getString, isRecord } from '../../../lib/utils/type-utils'; /** A vendor that cannot answer in this long is failing, not slow. */ const TOKEN_EXCHANGE_TIMEOUT_MS = 15_000; diff --git a/services/platform/convex/identities/external_identities.ts b/services/platform/backend/core/identities/external_identities.ts similarity index 100% rename from services/platform/convex/identities/external_identities.ts rename to services/platform/backend/core/identities/external_identities.ts diff --git a/services/platform/convex/identities/external_identities_helpers.ts b/services/platform/backend/core/identities/external_identities_helpers.ts similarity index 100% rename from services/platform/convex/identities/external_identities_helpers.ts rename to services/platform/backend/core/identities/external_identities_helpers.ts diff --git a/services/platform/convex/knowledge/connection.test.ts b/services/platform/backend/core/knowledge/connection.test.ts similarity index 98% rename from services/platform/convex/knowledge/connection.test.ts rename to services/platform/backend/core/knowledge/connection.test.ts index 9627b99fc7..cb546815df 100644 --- a/services/platform/convex/knowledge/connection.test.ts +++ b/services/platform/backend/core/knowledge/connection.test.ts @@ -6,7 +6,7 @@ import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { KnowledgeConnection } from '../../lib/shared/schemas/knowledge'; +import type { KnowledgeConnection } from '../../../lib/shared/schemas/knowledge'; import { buildConnectionUrl, readOrgConnection, diff --git a/services/platform/convex/knowledge/connection.ts b/services/platform/backend/core/knowledge/connection.ts similarity index 98% rename from services/platform/convex/knowledge/connection.ts rename to services/platform/backend/core/knowledge/connection.ts index 3814d85af7..b6fa936d9a 100644 --- a/services/platform/convex/knowledge/connection.ts +++ b/services/platform/backend/core/knowledge/connection.ts @@ -23,7 +23,7 @@ import path from 'node:path'; -import { zodErrorMessage } from '../../lib/shared/schemas/format-error'; +import { zodErrorMessage } from '../../../lib/shared/schemas/format-error'; import { KNOWLEDGE_CONFIG_DOMAIN, KNOWLEDGE_CONNECTION_KEY, @@ -33,7 +33,7 @@ import { knowledgeEmbeddingSchema, type KnowledgeConnection, type KnowledgeEmbeddingConfig, -} from '../../lib/shared/schemas/knowledge'; +} from '../../../lib/shared/schemas/knowledge'; import { errnoCode, getConfigRoot, diff --git a/services/platform/convex/knowledge/corpus.test.ts b/services/platform/backend/core/knowledge/corpus.test.ts similarity index 100% rename from services/platform/convex/knowledge/corpus.test.ts rename to services/platform/backend/core/knowledge/corpus.test.ts diff --git a/services/platform/convex/knowledge/corpus.ts b/services/platform/backend/core/knowledge/corpus.ts similarity index 99% rename from services/platform/convex/knowledge/corpus.ts rename to services/platform/backend/core/knowledge/corpus.ts index 85a7cd588f..fa51005d0d 100644 --- a/services/platform/convex/knowledge/corpus.ts +++ b/services/platform/backend/core/knowledge/corpus.ts @@ -29,17 +29,17 @@ import type { Sql } from 'postgres'; -import { logger } from '../../lib/knowledge/logger'; +import { logger } from '../../../lib/knowledge/logger'; import type { CorpusLegQuery, CorpusReader, -} from '../../lib/knowledge/retrieve'; +} from '../../../lib/knowledge/retrieve'; import { PRIVATE_KNOWLEDGE_SCHEMA, PUBLIC_WEB_SCHEMA, type KnowledgeCorpus, type KnowledgeHit, -} from '../../lib/knowledge/types'; +} from '../../../lib/knowledge/types'; import { bm25Available, isDataCorrupted, diff --git a/services/platform/convex/knowledge/crawl.ts b/services/platform/backend/core/knowledge/crawl.ts similarity index 99% rename from services/platform/convex/knowledge/crawl.ts rename to services/platform/backend/core/knowledge/crawl.ts index 55fe6165b5..4cdc57de9e 100644 --- a/services/platform/convex/knowledge/crawl.ts +++ b/services/platform/backend/core/knowledge/crawl.ts @@ -17,7 +17,7 @@ import type { Sql } from 'postgres'; -import { PUBLIC_WEB_SCHEMA } from '../../lib/knowledge/types'; +import { PUBLIC_WEB_SCHEMA } from '../../../lib/knowledge/types'; import type { CrawlerChunk, CrawlerPage, diff --git a/services/platform/convex/knowledge/crawl_action.ts b/services/platform/backend/core/knowledge/crawl_action.ts similarity index 99% rename from services/platform/convex/knowledge/crawl_action.ts rename to services/platform/backend/core/knowledge/crawl_action.ts index 12a7f7ffdc..6e95849dcb 100644 --- a/services/platform/convex/knowledge/crawl_action.ts +++ b/services/platform/backend/core/knowledge/crawl_action.ts @@ -38,7 +38,7 @@ import { computeContentHash } from '@tale/shared/utils/hashing'; import type { Sql } from 'postgres'; -import { chunkDocument } from '../../lib/knowledge/chunking'; +import { chunkDocument } from '../../../lib/knowledge/chunking'; import { classifyContentType, documentNameForUrl, @@ -51,17 +51,17 @@ import { parseSitemapLocs, siteHosts, stripBoilerplate, -} from '../../lib/knowledge/crawl-parse'; -import { htmlTitle, htmlToText } from '../../lib/knowledge/html-to-text'; -import { PUBLIC_WEB_SCHEMA } from '../../lib/knowledge/types'; -import type { ActionCtx } from '../lib/ctx'; -import { internal } from '../lib/handler_names'; -import { orgSlugFromIdOrNull } from '../lib/helpers/org_slug'; +} from '../../../lib/knowledge/crawl-parse'; +import { htmlTitle, htmlToText } from '../../../lib/knowledge/html-to-text'; +import { PUBLIC_WEB_SCHEMA } from '../../../lib/knowledge/types'; import { safeFetch, safeFetchBinary, SafeFetchError, -} from '../lib/http/safe_fetch'; +} from '../../../lib/net/safe-fetch'; +import type { ActionCtx } from '../lib/ctx'; +import { internal } from '../lib/handler_names'; +import { orgSlugFromIdOrNull } from '../lib/helpers/org_slug'; import { extractText } from '../lib/knowledge/extraction/router'; import { renderUrlsInSandbox } from '../node_only/sandbox/render_fetch'; import { isDueForScan } from '../websites/scan_scheduling'; diff --git a/services/platform/convex/knowledge/ddl.test.ts b/services/platform/backend/core/knowledge/ddl.test.ts similarity index 100% rename from services/platform/convex/knowledge/ddl.test.ts rename to services/platform/backend/core/knowledge/ddl.test.ts diff --git a/services/platform/convex/knowledge/ddl.ts b/services/platform/backend/core/knowledge/ddl.ts similarity index 99% rename from services/platform/convex/knowledge/ddl.ts rename to services/platform/backend/core/knowledge/ddl.ts index 936a6e8977..4f25c71037 100644 --- a/services/platform/convex/knowledge/ddl.ts +++ b/services/platform/backend/core/knowledge/ddl.ts @@ -45,11 +45,11 @@ import { fileURLToPath } from 'node:url'; import type { Sql } from 'postgres'; -import { logger } from '../../lib/knowledge/logger'; +import { logger } from '../../../lib/knowledge/logger'; import { PRIVATE_KNOWLEDGE_SCHEMA, PUBLIC_WEB_SCHEMA, -} from '../../lib/knowledge/types'; +} from '../../../lib/knowledge/types'; /** The corpora whose migrations are applied, in the order they must run. */ const CORPUS_SCHEMAS = [PRIVATE_KNOWLEDGE_SCHEMA, PUBLIC_WEB_SCHEMA] as const; diff --git a/services/platform/convex/knowledge/dimensions.test.ts b/services/platform/backend/core/knowledge/dimensions.test.ts similarity index 100% rename from services/platform/convex/knowledge/dimensions.test.ts rename to services/platform/backend/core/knowledge/dimensions.test.ts diff --git a/services/platform/convex/knowledge/dimensions.ts b/services/platform/backend/core/knowledge/dimensions.ts similarity index 98% rename from services/platform/convex/knowledge/dimensions.ts rename to services/platform/backend/core/knowledge/dimensions.ts index 54dd93e11c..6498defafd 100644 --- a/services/platform/convex/knowledge/dimensions.ts +++ b/services/platform/backend/core/knowledge/dimensions.ts @@ -30,8 +30,8 @@ import type { Sql } from 'postgres'; -import { logger } from '../../lib/knowledge/logger'; -import { PRIVATE_KNOWLEDGE_SCHEMA } from '../../lib/knowledge/types'; +import { logger } from '../../../lib/knowledge/logger'; +import { PRIVATE_KNOWLEDGE_SCHEMA } from '../../../lib/knowledge/types'; import { isProgramLimitExceeded, isUndefinedTable } from './pool'; /** pgvector cannot build an HNSW index above this width. */ diff --git a/services/platform/convex/knowledge/embedding.ts b/services/platform/backend/core/knowledge/embedding.ts similarity index 97% rename from services/platform/convex/knowledge/embedding.ts rename to services/platform/backend/core/knowledge/embedding.ts index cb1f3b1d26..c0144edce2 100644 --- a/services/platform/convex/knowledge/embedding.ts +++ b/services/platform/backend/core/knowledge/embedding.ts @@ -25,10 +25,10 @@ import OpenAI from 'openai'; -import { logger } from '../../lib/knowledge/logger'; -import type { QueryEmbedder } from '../../lib/knowledge/retrieve'; -import type { EmbeddingModel } from '../../lib/knowledge/types'; -import type { KnowledgeEmbeddingConfig } from '../../lib/shared/schemas/knowledge'; +import { logger } from '../../../lib/knowledge/logger'; +import type { QueryEmbedder } from '../../../lib/knowledge/retrieve'; +import type { EmbeddingModel } from '../../../lib/knowledge/types'; +import type { KnowledgeEmbeddingConfig } from '../../../lib/shared/schemas/knowledge'; import type { ActionCtx } from '../lib/ctx'; import { resolveProvidersForOrgId } from '../lib/providers/org_providers'; import { resolveProviderCredential } from '../provider_credentials/resolve_credential'; diff --git a/services/platform/convex/knowledge/fetch.test.ts b/services/platform/backend/core/knowledge/fetch.test.ts similarity index 99% rename from services/platform/convex/knowledge/fetch.test.ts rename to services/platform/backend/core/knowledge/fetch.test.ts index c7599b94f4..874d9c0035 100644 --- a/services/platform/convex/knowledge/fetch.test.ts +++ b/services/platform/backend/core/knowledge/fetch.test.ts @@ -7,7 +7,7 @@ import path from 'node:path'; import type { Sql } from 'postgres'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { KnowledgeAccessScope } from '../../lib/knowledge/types'; +import type { KnowledgeAccessScope } from '../../../lib/knowledge/types'; import { fetchDocumentByFileId as fetchDocumentByFileIdImpl, fetchWebPageByUrl, diff --git a/services/platform/convex/knowledge/fetch.ts b/services/platform/backend/core/knowledge/fetch.ts similarity index 99% rename from services/platform/convex/knowledge/fetch.ts rename to services/platform/backend/core/knowledge/fetch.ts index c9e6aa848c..79fec5ef04 100644 --- a/services/platform/convex/knowledge/fetch.ts +++ b/services/platform/backend/core/knowledge/fetch.ts @@ -26,7 +26,7 @@ import { PUBLIC_WEB_SCHEMA, knowledgeScopeAllows, type KnowledgeAccessScope, -} from '../../lib/knowledge/types'; +} from '../../../lib/knowledge/types'; import type { ActionCtx } from '../lib/ctx'; import { internal } from '../lib/handler_names'; import { diff --git a/services/platform/convex/knowledge/indexing.test.ts b/services/platform/backend/core/knowledge/indexing.test.ts similarity index 99% rename from services/platform/convex/knowledge/indexing.test.ts rename to services/platform/backend/core/knowledge/indexing.test.ts index c475bd04e1..631ff539ce 100644 --- a/services/platform/convex/knowledge/indexing.test.ts +++ b/services/platform/backend/core/knowledge/indexing.test.ts @@ -4,7 +4,7 @@ import { computeContentHash } from '@tale/shared/utils/hashing'; import type { Sql } from 'postgres'; import { describe, expect, it } from 'vitest'; -import type { EmbeddingModel } from '../../lib/knowledge/types'; +import type { EmbeddingModel } from '../../../lib/knowledge/types'; import type { Embedder } from './embedding'; import { indexDocument } from './indexing'; diff --git a/services/platform/convex/knowledge/indexing.ts b/services/platform/backend/core/knowledge/indexing.ts similarity index 97% rename from services/platform/convex/knowledge/indexing.ts rename to services/platform/backend/core/knowledge/indexing.ts index e3ac576be8..9944418e30 100644 --- a/services/platform/convex/knowledge/indexing.ts +++ b/services/platform/backend/core/knowledge/indexing.ts @@ -36,12 +36,12 @@ import type { Sql } from 'postgres'; import { chunkDocument, type ContextualChunk, -} from '../../lib/knowledge/chunking'; -import { planIngest, sliceToStore } from '../../lib/knowledge/ingest-plan'; -import { logger } from '../../lib/knowledge/logger'; -import { scanForSecrets } from '../../lib/knowledge/secret-scan'; -import { PRIVATE_KNOWLEDGE_SCHEMA as SCHEMA } from '../../lib/knowledge/types'; -import type { PiiConfig } from '../../lib/shared/schemas/pii'; +} from '../../../lib/knowledge/chunking'; +import { planIngest, sliceToStore } from '../../../lib/knowledge/ingest-plan'; +import { logger } from '../../../lib/knowledge/logger'; +import { scanForSecrets } from '../../../lib/knowledge/secret-scan'; +import { PRIVATE_KNOWLEDGE_SCHEMA as SCHEMA } from '../../../lib/knowledge/types'; +import type { PiiConfig } from '../../../lib/shared/schemas/pii'; import { assertVectorWidth } from './dimensions'; import type { Embedder } from './embedding'; import { applyPiiPolicyForIndexing } from './pii_gate'; @@ -299,7 +299,7 @@ async function readStoredState( orgSlug: string, fileId: string, ): Promise< - import('../../lib/knowledge/ingest-plan').StoredDocumentState | null + import('../../../lib/knowledge/ingest-plan').StoredDocumentState | null > { const rows = await sql.unsafe< { diff --git a/services/platform/convex/knowledge/pii_gate.test.ts b/services/platform/backend/core/knowledge/pii_gate.test.ts similarity index 98% rename from services/platform/convex/knowledge/pii_gate.test.ts rename to services/platform/backend/core/knowledge/pii_gate.test.ts index ce478d5170..2a83c4a5f7 100644 --- a/services/platform/convex/knowledge/pii_gate.test.ts +++ b/services/platform/backend/core/knowledge/pii_gate.test.ts @@ -88,7 +88,7 @@ describe('applyPiiPolicyForIndexing', () => { // failure is forced here. What matters is the guarantee: failing the index // would take an organization's corpus offline over a governance typo. vi.resetModules(); - vi.doMock('../../lib/pii', () => ({ + vi.doMock('../../../lib/pii', () => ({ createScrubberFromConfig: () => { throw new Error('unknown locale code'); }, @@ -102,7 +102,7 @@ describe('applyPiiPolicyForIndexing', () => { }); expect(warn).toHaveBeenCalled(); warn.mockRestore(); - vi.doUnmock('../../lib/pii'); + vi.doUnmock('../../../lib/pii'); vi.resetModules(); }); }); diff --git a/services/platform/convex/knowledge/pii_gate.ts b/services/platform/backend/core/knowledge/pii_gate.ts similarity index 97% rename from services/platform/convex/knowledge/pii_gate.ts rename to services/platform/backend/core/knowledge/pii_gate.ts index 947ab2239c..53ec5a6b23 100644 --- a/services/platform/convex/knowledge/pii_gate.ts +++ b/services/platform/backend/core/knowledge/pii_gate.ts @@ -40,8 +40,11 @@ * indexed copy masked is the safe reading. */ -import { createScrubberFromConfig } from '../../lib/pii'; -import { piiConfigSchema, type PiiConfig } from '../../lib/shared/schemas/pii'; +import { createScrubberFromConfig } from '../../../lib/pii'; +import { + piiConfigSchema, + type PiiConfig, +} from '../../../lib/shared/schemas/pii'; export type PiiIngestDecision = | { readonly kind: 'index'; readonly text: string } diff --git a/services/platform/convex/knowledge/pool.test.ts b/services/platform/backend/core/knowledge/pool.test.ts similarity index 100% rename from services/platform/convex/knowledge/pool.test.ts rename to services/platform/backend/core/knowledge/pool.test.ts diff --git a/services/platform/convex/knowledge/pool.ts b/services/platform/backend/core/knowledge/pool.ts similarity index 98% rename from services/platform/convex/knowledge/pool.ts rename to services/platform/backend/core/knowledge/pool.ts index 634a3f44af..4ac4b82023 100644 --- a/services/platform/convex/knowledge/pool.ts +++ b/services/platform/backend/core/knowledge/pool.ts @@ -41,19 +41,19 @@ import postgres, { type Sql } from 'postgres'; -import { logger } from '../../lib/knowledge/logger'; +import { logger } from '../../../lib/knowledge/logger'; import { buildConnectionUrl, readOrgConnection } from './connection'; import { applyCorpusSchema } from './ddl'; export { PRIVATE_KNOWLEDGE_SCHEMA, PUBLIC_WEB_SCHEMA, -} from '../../lib/knowledge/types'; +} from '../../../lib/knowledge/types'; import { PRIVATE_KNOWLEDGE_SCHEMA, PUBLIC_WEB_SCHEMA, -} from '../../lib/knowledge/types'; +} from '../../../lib/knowledge/types'; /** * How a pool is actually opened. Replaceable so tests can observe which diff --git a/services/platform/convex/knowledge/rag_error_codes.ts b/services/platform/backend/core/knowledge/rag_error_codes.ts similarity index 100% rename from services/platform/convex/knowledge/rag_error_codes.ts rename to services/platform/backend/core/knowledge/rag_error_codes.ts diff --git a/services/platform/convex/knowledge/search.test.ts b/services/platform/backend/core/knowledge/search.test.ts similarity index 97% rename from services/platform/convex/knowledge/search.test.ts rename to services/platform/backend/core/knowledge/search.test.ts index 13fe5c25a6..171bdcc127 100644 --- a/services/platform/convex/knowledge/search.test.ts +++ b/services/platform/backend/core/knowledge/search.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const retrieveMock = vi.fn(); -vi.mock('../../lib/knowledge/retrieve', () => ({ +vi.mock('../../../lib/knowledge/retrieve', () => ({ retrieve: (...args: unknown[]) => retrieveMock(...args), })); diff --git a/services/platform/convex/knowledge/search.ts b/services/platform/backend/core/knowledge/search.ts similarity index 96% rename from services/platform/convex/knowledge/search.ts rename to services/platform/backend/core/knowledge/search.ts index 1d6694131d..ab5c71301c 100644 --- a/services/platform/convex/knowledge/search.ts +++ b/services/platform/backend/core/knowledge/search.ts @@ -18,7 +18,7 @@ * The `get_knowledge` capability's backend is exactly: * * ```ts - * import { searchKnowledge } from '../knowledge/search'; + * import { searchKnowledge } from './search'; * * const result = await searchKnowledge(ctx, { * organizationId, // the Convex organization id, for the credential @@ -43,18 +43,18 @@ * organization's corpus with another's credential. */ -import { retrieve, type CorpusReader } from '../../lib/knowledge/retrieve'; +import { retrieve, type CorpusReader } from '../../../lib/knowledge/retrieve'; import type { KnowledgeSearchBackend, KnowledgeSearchInput, -} from '../../lib/knowledge/search-node'; +} from '../../../lib/knowledge/search-node'; import { PRIVATE_KNOWLEDGE_SCHEMA, corporaFor, type KnowledgeQuery, type KnowledgeResult, -} from '../../lib/knowledge/types'; -import type { KnowledgeEmbeddingConfig } from '../../lib/shared/schemas/knowledge'; +} from '../../../lib/knowledge/types'; +import type { KnowledgeEmbeddingConfig } from '../../../lib/shared/schemas/knowledge'; import type { ActionCtx } from '../lib/ctx'; import { internal } from '../lib/handler_names'; import { readOrgEmbeddingConfig } from './connection'; diff --git a/services/platform/convex/knowledge_entries/constants.ts b/services/platform/backend/core/knowledge_entries/constants.ts similarity index 100% rename from services/platform/convex/knowledge_entries/constants.ts rename to services/platform/backend/core/knowledge_entries/constants.ts diff --git a/services/platform/convex/knowledge_entries/helpers.test.ts b/services/platform/backend/core/knowledge_entries/helpers.test.ts similarity index 99% rename from services/platform/convex/knowledge_entries/helpers.test.ts rename to services/platform/backend/core/knowledge_entries/helpers.test.ts index 1fb51bd1f6..72b6498ff5 100644 --- a/services/platform/convex/knowledge_entries/helpers.test.ts +++ b/services/platform/backend/core/knowledge_entries/helpers.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { normalizeTopicKey } from './constants'; import { findActiveEntryByTopicKey, diff --git a/services/platform/convex/knowledge_entries/helpers.ts b/services/platform/backend/core/knowledge_entries/helpers.ts similarity index 98% rename from services/platform/convex/knowledge_entries/helpers.ts rename to services/platform/backend/core/knowledge_entries/helpers.ts index 7a55523b1e..654e491dcc 100644 --- a/services/platform/convex/knowledge_entries/helpers.ts +++ b/services/platform/backend/core/knowledge_entries/helpers.ts @@ -5,7 +5,7 @@ * bundle (or their tests' mock surface). */ -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import type { MutationCtx } from '../lib/ctx'; import type { Doc, Id } from '../lib/rows'; import { diff --git a/services/platform/convex/legacy/knowledge_delete.ts b/services/platform/backend/core/legacy/knowledge_delete.ts similarity index 98% rename from services/platform/convex/legacy/knowledge_delete.ts rename to services/platform/backend/core/legacy/knowledge_delete.ts index 7ca12d5eac..8e1587409a 100644 --- a/services/platform/convex/legacy/knowledge_delete.ts +++ b/services/platform/backend/core/legacy/knowledge_delete.ts @@ -59,8 +59,8 @@ import path from 'node:path'; import postgres from 'postgres'; import type { z } from 'zod/v4'; -import { pgConnectionSchema } from '../../lib/shared/schemas/deployment'; -import { zodErrorMessage } from '../../lib/shared/schemas/format-error'; +import { pgConnectionSchema } from '../../../lib/shared/schemas/deployment'; +import { zodErrorMessage } from '../../../lib/shared/schemas/format-error'; import { errnoCode, getConfigRoot, diff --git a/services/platform/convex/lib/age_keygen.ts b/services/platform/backend/core/lib/age_keygen.ts similarity index 100% rename from services/platform/convex/lib/age_keygen.ts rename to services/platform/backend/core/lib/age_keygen.ts diff --git a/services/platform/convex/lib/auth/find_user_by_normalized_email.ts b/services/platform/backend/core/lib/auth/find_user_by_normalized_email.ts similarity index 100% rename from services/platform/convex/lib/auth/find_user_by_normalized_email.ts rename to services/platform/backend/core/lib/auth/find_user_by_normalized_email.ts diff --git a/services/platform/convex/lib/auth/normalize_auth_email.test.ts b/services/platform/backend/core/lib/auth/normalize_auth_email.test.ts similarity index 100% rename from services/platform/convex/lib/auth/normalize_auth_email.test.ts rename to services/platform/backend/core/lib/auth/normalize_auth_email.test.ts diff --git a/services/platform/convex/lib/auth/normalize_auth_email.ts b/services/platform/backend/core/lib/auth/normalize_auth_email.ts similarity index 100% rename from services/platform/convex/lib/auth/normalize_auth_email.ts rename to services/platform/backend/core/lib/auth/normalize_auth_email.ts diff --git a/services/platform/convex/lib/auth/require_org_admin_or_developer.test.ts b/services/platform/backend/core/lib/auth/require_org_admin_or_developer.test.ts similarity index 100% rename from services/platform/convex/lib/auth/require_org_admin_or_developer.test.ts rename to services/platform/backend/core/lib/auth/require_org_admin_or_developer.test.ts diff --git a/services/platform/convex/lib/auth/require_org_admin_or_developer.ts b/services/platform/backend/core/lib/auth/require_org_admin_or_developer.ts similarity index 92% rename from services/platform/convex/lib/auth/require_org_admin_or_developer.ts rename to services/platform/backend/core/lib/auth/require_org_admin_or_developer.ts index 486d8d344a..a36d87b711 100644 --- a/services/platform/convex/lib/auth/require_org_admin_or_developer.ts +++ b/services/platform/backend/core/lib/auth/require_org_admin_or_developer.ts @@ -17,8 +17,8 @@ * and V8 actions. */ -import { defineAbilityFor } from '../../../lib/permissions/ability'; -import { AppError } from '../../../lib/shared/errors/app-error'; +import { defineAbilityFor } from '../../../../lib/permissions/ability'; +import { AppError } from '../../../../lib/shared/errors/app-error'; import type { ActionCtx, MutationCtx } from '../ctx'; import { requireOrgMembershipById, diff --git a/services/platform/convex/lib/auth/require_org_membership.test.ts b/services/platform/backend/core/lib/auth/require_org_membership.test.ts similarity index 98% rename from services/platform/convex/lib/auth/require_org_membership.test.ts rename to services/platform/backend/core/lib/auth/require_org_membership.test.ts index ac7425d24d..598e0e3973 100644 --- a/services/platform/convex/lib/auth/require_org_membership.test.ts +++ b/services/platform/backend/core/lib/auth/require_org_membership.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { AppError } from '../../../lib/shared/errors/app-error'; +import { AppError } from '../../../../lib/shared/errors/app-error'; const mockGetAuthUser = vi.fn(); diff --git a/services/platform/convex/lib/auth/require_org_membership.ts b/services/platform/backend/core/lib/auth/require_org_membership.ts similarity index 98% rename from services/platform/convex/lib/auth/require_org_membership.ts rename to services/platform/backend/core/lib/auth/require_org_membership.ts index 2019d0d38e..7bb71c958c 100644 --- a/services/platform/convex/lib/auth/require_org_membership.ts +++ b/services/platform/backend/core/lib/auth/require_org_membership.ts @@ -32,7 +32,7 @@ * and V8 actions. */ -import { AppError } from '../../../lib/shared/errors/app-error'; +import { AppError } from '../../../../lib/shared/errors/app-error'; import type { ActionCtx, MutationCtx } from '../ctx'; import { components } from '../handler_names'; import { getAuthUserIdentity } from '../rls/auth/get_auth_user_identity'; diff --git a/services/platform/convex/lib/config_cache/read.ts b/services/platform/backend/core/lib/config_cache/read.ts similarity index 100% rename from services/platform/convex/lib/config_cache/read.ts rename to services/platform/backend/core/lib/config_cache/read.ts diff --git a/services/platform/convex/lib/config_store/builtin_catalog.ts b/services/platform/backend/core/lib/config_store/builtin_catalog.ts similarity index 100% rename from services/platform/convex/lib/config_store/builtin_catalog.ts rename to services/platform/backend/core/lib/config_store/builtin_catalog.ts diff --git a/services/platform/convex/lib/config_store/read_domain_file.test.ts b/services/platform/backend/core/lib/config_store/read_domain_file.test.ts similarity index 100% rename from services/platform/convex/lib/config_store/read_domain_file.test.ts rename to services/platform/backend/core/lib/config_store/read_domain_file.test.ts diff --git a/services/platform/convex/lib/config_store/read_domain_file.ts b/services/platform/backend/core/lib/config_store/read_domain_file.ts similarity index 97% rename from services/platform/convex/lib/config_store/read_domain_file.ts rename to services/platform/backend/core/lib/config_store/read_domain_file.ts index d636b5e86b..8d36ad5178 100644 --- a/services/platform/convex/lib/config_store/read_domain_file.ts +++ b/services/platform/backend/core/lib/config_store/read_domain_file.ts @@ -22,7 +22,7 @@ * same `readJsonFile` guards every JSON reader already used. */ -import { parseYamlOrThrow } from '../../../lib/shared/config/yaml'; +import { parseYamlOrThrow } from '../../../../lib/shared/config/yaml'; import { readJsonFile, safeJoinWithinDir, diff --git a/services/platform/convex/lib/config_store/resolvers.ts b/services/platform/backend/core/lib/config_store/resolvers.ts similarity index 100% rename from services/platform/convex/lib/config_store/resolvers.ts rename to services/platform/backend/core/lib/config_store/resolvers.ts diff --git a/services/platform/convex/lib/crypto/base64_to_bytes.ts b/services/platform/backend/core/lib/crypto/base64_to_bytes.ts similarity index 100% rename from services/platform/convex/lib/crypto/base64_to_bytes.ts rename to services/platform/backend/core/lib/crypto/base64_to_bytes.ts diff --git a/services/platform/convex/lib/crypto/base64_url_to_buffer.ts b/services/platform/backend/core/lib/crypto/base64_url_to_buffer.ts similarity index 100% rename from services/platform/convex/lib/crypto/base64_url_to_buffer.ts rename to services/platform/backend/core/lib/crypto/base64_url_to_buffer.ts diff --git a/services/platform/convex/lib/crypto/decrypt_string.ts b/services/platform/backend/core/lib/crypto/decrypt_string.ts similarity index 100% rename from services/platform/convex/lib/crypto/decrypt_string.ts rename to services/platform/backend/core/lib/crypto/decrypt_string.ts diff --git a/services/platform/convex/lib/crypto/disarm_broken_to_base64_shim.ts b/services/platform/backend/core/lib/crypto/disarm_broken_to_base64_shim.ts similarity index 100% rename from services/platform/convex/lib/crypto/disarm_broken_to_base64_shim.ts rename to services/platform/backend/core/lib/crypto/disarm_broken_to_base64_shim.ts diff --git a/services/platform/convex/lib/crypto/encrypt_string.ts b/services/platform/backend/core/lib/crypto/encrypt_string.ts similarity index 100% rename from services/platform/convex/lib/crypto/encrypt_string.ts rename to services/platform/backend/core/lib/crypto/encrypt_string.ts diff --git a/services/platform/convex/lib/crypto/get_secret_key.ts b/services/platform/backend/core/lib/crypto/get_secret_key.ts similarity index 100% rename from services/platform/convex/lib/crypto/get_secret_key.ts rename to services/platform/backend/core/lib/crypto/get_secret_key.ts diff --git a/services/platform/convex/lib/crypto/hex_to_bytes.ts b/services/platform/backend/core/lib/crypto/hex_to_bytes.ts similarity index 100% rename from services/platform/convex/lib/crypto/hex_to_bytes.ts rename to services/platform/backend/core/lib/crypto/hex_to_bytes.ts diff --git a/services/platform/convex/lib/ctx.ts b/services/platform/backend/core/lib/ctx.ts similarity index 99% rename from services/platform/convex/lib/ctx.ts rename to services/platform/backend/core/lib/ctx.ts index 6d0e3c0d17..92a861d770 100644 --- a/services/platform/convex/lib/ctx.ts +++ b/services/platform/backend/core/lib/ctx.ts @@ -3,7 +3,7 @@ /** * The context types the reused 0.4 handler bodies are written against. * - * This is a description of what the 0.5 ctx shim (`backend/lib/convex-shim.ts`) + * This is a description of what the 0.5 ctx shim (`backend/lib/ctx-shim.ts`) * actually hands a body, not a re-export of the retired runtime's types. Two * things differ from what the generator used to emit, and both differences are * the point: diff --git a/services/platform/convex/lib/debug_log.ts b/services/platform/backend/core/lib/debug_log.ts similarity index 100% rename from services/platform/convex/lib/debug_log.ts rename to services/platform/backend/core/lib/debug_log.ts diff --git a/services/platform/convex/lib/e2e_cron_guard.ts b/services/platform/backend/core/lib/e2e_cron_guard.ts similarity index 100% rename from services/platform/convex/lib/e2e_cron_guard.ts rename to services/platform/backend/core/lib/e2e_cron_guard.ts diff --git a/services/platform/convex/lib/errors/classify_transcription_error.test.ts b/services/platform/backend/core/lib/errors/classify_transcription_error.test.ts similarity index 95% rename from services/platform/convex/lib/errors/classify_transcription_error.test.ts rename to services/platform/backend/core/lib/errors/classify_transcription_error.test.ts index a30cbbee9d..737f11d894 100644 --- a/services/platform/convex/lib/errors/classify_transcription_error.test.ts +++ b/services/platform/backend/core/lib/errors/classify_transcription_error.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { AppError } from '../../../lib/shared/errors/app-error'; +import { AppError } from '../../../../lib/shared/errors/app-error'; import { classifyTranscriptionError } from './classify_transcription_error'; describe('classifyTranscriptionError', () => { diff --git a/services/platform/convex/lib/errors/classify_transcription_error.ts b/services/platform/backend/core/lib/errors/classify_transcription_error.ts similarity index 100% rename from services/platform/convex/lib/errors/classify_transcription_error.ts rename to services/platform/backend/core/lib/errors/classify_transcription_error.ts diff --git a/services/platform/convex/lib/file_io.test.ts b/services/platform/backend/core/lib/file_io.test.ts similarity index 100% rename from services/platform/convex/lib/file_io.test.ts rename to services/platform/backend/core/lib/file_io.test.ts diff --git a/services/platform/convex/lib/file_io.ts b/services/platform/backend/core/lib/file_io.ts similarity index 98% rename from services/platform/convex/lib/file_io.ts rename to services/platform/backend/core/lib/file_io.ts index 76b4a84d7c..d434483675 100644 --- a/services/platform/convex/lib/file_io.ts +++ b/services/platform/backend/core/lib/file_io.ts @@ -23,8 +23,8 @@ import { } from 'node:fs/promises'; import path from 'node:path'; -import { isValidOrgSlug as sharedIsValidOrgSlug } from '../../lib/shared/constants/org-slug'; -import { sortObjectKeysDeep } from '../../lib/shared/utils/canonicalize-config'; +import { isValidOrgSlug as sharedIsValidOrgSlug } from '../../../lib/shared/constants/org-slug'; +import { sortObjectKeysDeep } from '../../../lib/shared/utils/canonicalize-config'; const TIMESTAMP_REGEX = /^\d{13,}(-[a-f0-9]+)?$/; diff --git a/services/platform/convex/lib/get_user_teams.ts b/services/platform/backend/core/lib/get_user_teams.ts similarity index 98% rename from services/platform/convex/lib/get_user_teams.ts rename to services/platform/backend/core/lib/get_user_teams.ts index a87e852cac..d20ae566b3 100644 --- a/services/platform/convex/lib/get_user_teams.ts +++ b/services/platform/backend/core/lib/get_user_teams.ts @@ -1,4 +1,4 @@ -import { getString, isRecord, parseJson } from '../../lib/utils/type-utils'; +import { getString, isRecord, parseJson } from '../../../lib/utils/type-utils'; import type { QueryCtx } from './ctx'; import { components } from './handler_names'; diff --git a/services/platform/convex/lib/handler_names.ts b/services/platform/backend/core/lib/handler_names.ts similarity index 99% rename from services/platform/convex/lib/handler_names.ts rename to services/platform/backend/core/lib/handler_names.ts index 2939a21c7e..831af529d2 100644 --- a/services/platform/convex/lib/handler_names.ts +++ b/services/platform/backend/core/lib/handler_names.ts @@ -2,7 +2,7 @@ * The names the reused 0.4 handlers address each other by. * * `internal.a.b.c` is a proxy walk that records a path; the 0.5 ctx shim - * (`backend/lib/convex-shim.ts`) turns that path into `a/b:c` and dispatches + * (`backend/lib/ctx-shim.ts`) turns that path into `a/b:c` and dispatches * it to a SQL-backed handler. A name with no handler throws, loudly and by * name, at the call — never silently. * @@ -21,7 +21,7 @@ import { createComponentRefs, createFunctionRefs, type FunctionRef, -} from '../../lib/shared/handlers/function-refs'; +} from '../../../lib/shared/handlers/function-refs'; interface HandlerNames { agent_secrets: FunctionRef & { diff --git a/services/platform/convex/lib/helpers/audit_hash.test.ts b/services/platform/backend/core/lib/helpers/audit_hash.test.ts similarity index 100% rename from services/platform/convex/lib/helpers/audit_hash.test.ts rename to services/platform/backend/core/lib/helpers/audit_hash.test.ts diff --git a/services/platform/convex/lib/helpers/audit_hash.ts b/services/platform/backend/core/lib/helpers/audit_hash.ts similarity index 99% rename from services/platform/convex/lib/helpers/audit_hash.ts rename to services/platform/backend/core/lib/helpers/audit_hash.ts index 90388eada3..1701311fde 100644 --- a/services/platform/convex/lib/helpers/audit_hash.ts +++ b/services/platform/backend/core/lib/helpers/audit_hash.ts @@ -13,7 +13,7 @@ * alphabetically at every nesting level. */ -import { isRecord } from '../../../lib/utils/type-utils'; +import { isRecord } from '../../../../lib/utils/type-utils'; /** * Fields excluded from hash computation because they are part of the diff --git a/services/platform/convex/lib/helpers/build_audit_context.ts b/services/platform/backend/core/lib/helpers/build_audit_context.ts similarity index 100% rename from services/platform/convex/lib/helpers/build_audit_context.ts rename to services/platform/backend/core/lib/helpers/build_audit_context.ts diff --git a/services/platform/convex/lib/helpers/count_items_in_org.test.ts b/services/platform/backend/core/lib/helpers/count_items_in_org.test.ts similarity index 100% rename from services/platform/convex/lib/helpers/count_items_in_org.test.ts rename to services/platform/backend/core/lib/helpers/count_items_in_org.test.ts diff --git a/services/platform/convex/lib/helpers/count_items_in_org.ts b/services/platform/backend/core/lib/helpers/count_items_in_org.ts similarity index 100% rename from services/platform/convex/lib/helpers/count_items_in_org.ts rename to services/platform/backend/core/lib/helpers/count_items_in_org.ts diff --git a/services/platform/convex/lib/helpers/id_shape.ts b/services/platform/backend/core/lib/helpers/id_shape.ts similarity index 100% rename from services/platform/convex/lib/helpers/id_shape.ts rename to services/platform/backend/core/lib/helpers/id_shape.ts diff --git a/services/platform/convex/lib/helpers/org_slug.test.ts b/services/platform/backend/core/lib/helpers/org_slug.test.ts similarity index 100% rename from services/platform/convex/lib/helpers/org_slug.test.ts rename to services/platform/backend/core/lib/helpers/org_slug.test.ts diff --git a/services/platform/convex/lib/helpers/org_slug.ts b/services/platform/backend/core/lib/helpers/org_slug.ts similarity index 98% rename from services/platform/convex/lib/helpers/org_slug.ts rename to services/platform/backend/core/lib/helpers/org_slug.ts index 3c58433b02..391104650f 100644 --- a/services/platform/convex/lib/helpers/org_slug.ts +++ b/services/platform/backend/core/lib/helpers/org_slug.ts @@ -7,8 +7,8 @@ * carry `organizationId`; this helper bridges to the slug. */ -import { AppError } from '../../../lib/shared/errors/app-error'; -import { getString, isRecord } from '../../../lib/utils/type-utils'; +import { AppError } from '../../../../lib/shared/errors/app-error'; +import { getString, isRecord } from '../../../../lib/utils/type-utils'; import { components } from '../handler_names'; import { looksLikeConvexDocumentId } from './id_shape'; diff --git a/services/platform/convex/lib/helpers/pii_hash.test.ts b/services/platform/backend/core/lib/helpers/pii_hash.test.ts similarity index 100% rename from services/platform/convex/lib/helpers/pii_hash.test.ts rename to services/platform/backend/core/lib/helpers/pii_hash.test.ts diff --git a/services/platform/convex/lib/helpers/pii_hash.ts b/services/platform/backend/core/lib/helpers/pii_hash.ts similarity index 100% rename from services/platform/convex/lib/helpers/pii_hash.ts rename to services/platform/backend/core/lib/helpers/pii_hash.ts diff --git a/services/platform/convex/lib/helpers/public_storage_url.test.ts b/services/platform/backend/core/lib/helpers/public_storage_url.test.ts similarity index 100% rename from services/platform/convex/lib/helpers/public_storage_url.test.ts rename to services/platform/backend/core/lib/helpers/public_storage_url.test.ts diff --git a/services/platform/convex/lib/helpers/public_storage_url.ts b/services/platform/backend/core/lib/helpers/public_storage_url.ts similarity index 100% rename from services/platform/convex/lib/helpers/public_storage_url.ts rename to services/platform/backend/core/lib/helpers/public_storage_url.ts diff --git a/services/platform/convex/lib/json/json_path.ts b/services/platform/backend/core/lib/json/json_path.ts similarity index 96% rename from services/platform/convex/lib/json/json_path.ts rename to services/platform/backend/core/lib/json/json_path.ts index dbbbaada60..979c1dc844 100644 --- a/services/platform/convex/lib/json/json_path.ts +++ b/services/platform/backend/core/lib/json/json_path.ts @@ -8,7 +8,7 @@ * `'use node'` actions, and the platform server alike. */ -import { isRecord } from '../../../lib/utils/type-utils'; +import { isRecord } from '../../../../lib/utils/type-utils'; export class JsonPathError extends Error { constructor(message: string) { diff --git a/services/platform/convex/lib/knowledge/extraction/docx.test.ts b/services/platform/backend/core/lib/knowledge/extraction/docx.test.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/docx.test.ts rename to services/platform/backend/core/lib/knowledge/extraction/docx.test.ts diff --git a/services/platform/convex/lib/knowledge/extraction/docx.ts b/services/platform/backend/core/lib/knowledge/extraction/docx.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/docx.ts rename to services/platform/backend/core/lib/knowledge/extraction/docx.ts diff --git a/services/platform/convex/lib/knowledge/extraction/helpers.ts b/services/platform/backend/core/lib/knowledge/extraction/helpers.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/helpers.ts rename to services/platform/backend/core/lib/knowledge/extraction/helpers.ts diff --git a/services/platform/convex/lib/knowledge/extraction/image.ts b/services/platform/backend/core/lib/knowledge/extraction/image.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/image.ts rename to services/platform/backend/core/lib/knowledge/extraction/image.ts diff --git a/services/platform/convex/lib/knowledge/extraction/odt.ts b/services/platform/backend/core/lib/knowledge/extraction/odt.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/odt.ts rename to services/platform/backend/core/lib/knowledge/extraction/odt.ts diff --git a/services/platform/convex/lib/knowledge/extraction/ooxml.ts b/services/platform/backend/core/lib/knowledge/extraction/ooxml.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/ooxml.ts rename to services/platform/backend/core/lib/knowledge/extraction/ooxml.ts diff --git a/services/platform/convex/lib/knowledge/extraction/pdf.test.ts b/services/platform/backend/core/lib/knowledge/extraction/pdf.test.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/pdf.test.ts rename to services/platform/backend/core/lib/knowledge/extraction/pdf.test.ts diff --git a/services/platform/convex/lib/knowledge/extraction/pdf.ts b/services/platform/backend/core/lib/knowledge/extraction/pdf.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/pdf.ts rename to services/platform/backend/core/lib/knowledge/extraction/pdf.ts diff --git a/services/platform/convex/lib/knowledge/extraction/pdfjs_dom_polyfill.test.ts b/services/platform/backend/core/lib/knowledge/extraction/pdfjs_dom_polyfill.test.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/pdfjs_dom_polyfill.test.ts rename to services/platform/backend/core/lib/knowledge/extraction/pdfjs_dom_polyfill.test.ts diff --git a/services/platform/convex/lib/knowledge/extraction/pdfjs_dom_polyfill.ts b/services/platform/backend/core/lib/knowledge/extraction/pdfjs_dom_polyfill.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/pdfjs_dom_polyfill.ts rename to services/platform/backend/core/lib/knowledge/extraction/pdfjs_dom_polyfill.ts diff --git a/services/platform/convex/lib/knowledge/extraction/pdfjs_loader.ts b/services/platform/backend/core/lib/knowledge/extraction/pdfjs_loader.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/pdfjs_loader.ts rename to services/platform/backend/core/lib/knowledge/extraction/pdfjs_loader.ts diff --git a/services/platform/convex/lib/knowledge/extraction/pptx.test.ts b/services/platform/backend/core/lib/knowledge/extraction/pptx.test.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/pptx.test.ts rename to services/platform/backend/core/lib/knowledge/extraction/pptx.test.ts diff --git a/services/platform/convex/lib/knowledge/extraction/pptx.ts b/services/platform/backend/core/lib/knowledge/extraction/pptx.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/pptx.ts rename to services/platform/backend/core/lib/knowledge/extraction/pptx.ts diff --git a/services/platform/convex/lib/knowledge/extraction/router.test.ts b/services/platform/backend/core/lib/knowledge/extraction/router.test.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/router.test.ts rename to services/platform/backend/core/lib/knowledge/extraction/router.test.ts diff --git a/services/platform/convex/lib/knowledge/extraction/router.ts b/services/platform/backend/core/lib/knowledge/extraction/router.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/router.ts rename to services/platform/backend/core/lib/knowledge/extraction/router.ts diff --git a/services/platform/convex/lib/knowledge/extraction/text.ts b/services/platform/backend/core/lib/knowledge/extraction/text.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/text.ts rename to services/platform/backend/core/lib/knowledge/extraction/text.ts diff --git a/services/platform/convex/lib/knowledge/extraction/vision_client.ts b/services/platform/backend/core/lib/knowledge/extraction/vision_client.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/vision_client.ts rename to services/platform/backend/core/lib/knowledge/extraction/vision_client.ts diff --git a/services/platform/convex/lib/knowledge/extraction/xlsx.test.ts b/services/platform/backend/core/lib/knowledge/extraction/xlsx.test.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/xlsx.test.ts rename to services/platform/backend/core/lib/knowledge/extraction/xlsx.test.ts diff --git a/services/platform/convex/lib/knowledge/extraction/xlsx.ts b/services/platform/backend/core/lib/knowledge/extraction/xlsx.ts similarity index 100% rename from services/platform/convex/lib/knowledge/extraction/xlsx.ts rename to services/platform/backend/core/lib/knowledge/extraction/xlsx.ts diff --git a/services/platform/convex/lib/providers/agent_serving.test.ts b/services/platform/backend/core/lib/providers/agent_serving.test.ts similarity index 99% rename from services/platform/convex/lib/providers/agent_serving.test.ts rename to services/platform/backend/core/lib/providers/agent_serving.test.ts index be3d1346c0..804c7f841d 100644 --- a/services/platform/convex/lib/providers/agent_serving.test.ts +++ b/services/platform/backend/core/lib/providers/agent_serving.test.ts @@ -17,7 +17,7 @@ import { harnessDefinitionSchema, providerDefinitionSchema, type ProviderDefinition, -} from '../../../lib/shared/schemas/providers'; +} from '../../../../lib/shared/schemas/providers'; import type { ActionCtx } from '../ctx'; const { getProviderCatalog, resolveConnectors, loadHarnesses } = vi.hoisted( diff --git a/services/platform/convex/lib/providers/agent_serving.ts b/services/platform/backend/core/lib/providers/agent_serving.ts similarity index 98% rename from services/platform/convex/lib/providers/agent_serving.ts rename to services/platform/backend/core/lib/providers/agent_serving.ts index a5a40beda4..c2c61137b2 100644 --- a/services/platform/convex/lib/providers/agent_serving.ts +++ b/services/platform/backend/core/lib/providers/agent_serving.ts @@ -31,16 +31,16 @@ import { buildHarnessTable, resolveExecution, -} from '../../../lib/shared/providers/resolve_execution'; +} from '../../../../lib/shared/providers/resolve_execution'; import type { ModelCatalogEntry, ProviderDefinition, -} from '../../../lib/shared/schemas/providers'; +} from '../../../../lib/shared/schemas/providers'; import { modelAllowlistPermits, modelIdsEquivalent, -} from '../../../lib/shared/utils/model-ref'; -import { isRecord } from '../../../lib/utils/type-utils'; +} from '../../../../lib/shared/utils/model-ref'; +import { isRecord } from '../../../../lib/utils/type-utils'; import type { ActionCtx } from '../ctx'; import { internal } from '../handler_names'; import { getProviderCatalog } from './catalog_fetch'; diff --git a/services/platform/convex/lib/providers/catalog_fetch.test.ts b/services/platform/backend/core/lib/providers/catalog_fetch.test.ts similarity index 97% rename from services/platform/convex/lib/providers/catalog_fetch.test.ts rename to services/platform/backend/core/lib/providers/catalog_fetch.test.ts index ec02907a24..f2b71a6856 100644 --- a/services/platform/convex/lib/providers/catalog_fetch.test.ts +++ b/services/platform/backend/core/lib/providers/catalog_fetch.test.ts @@ -1,15 +1,16 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { providerDefinitionSchema } from '../../../lib/shared/schemas/providers'; -import { safeFetch, SafeFetchError } from '../http/safe_fetch'; +import { safeFetch, SafeFetchError } from '../../../../lib/net/safe-fetch'; +import { providerDefinitionSchema } from '../../../../lib/shared/schemas/providers'; import { CATALOG_TTL_MS, getProviderCatalog, invalidateCatalogFetchCache, } from './catalog_fetch'; -vi.mock('../http/safe_fetch', async (importOriginal) => { - const original = await importOriginal(); +vi.mock('../../../../lib/net/safe-fetch', async (importOriginal) => { + const original = + await importOriginal(); return { ...original, safeFetch: vi.fn() }; }); diff --git a/services/platform/convex/lib/providers/catalog_fetch.ts b/services/platform/backend/core/lib/providers/catalog_fetch.ts similarity index 97% rename from services/platform/convex/lib/providers/catalog_fetch.ts rename to services/platform/backend/core/lib/providers/catalog_fetch.ts index fe92b7f77e..2765db96ec 100644 --- a/services/platform/convex/lib/providers/catalog_fetch.ts +++ b/services/platform/backend/core/lib/providers/catalog_fetch.ts @@ -25,14 +25,14 @@ * credential material is ever attached to these requests. */ -import { isPrivateIp } from '../../../lib/shared/net/private-ip'; -import { normalizeCatalogPayload } from '../../../lib/shared/providers/catalog_normalize'; +import { checkProviderHostPolicy } from '../../../../lib/net/host-policy'; +import { safeFetch, SafeFetchError } from '../../../../lib/net/safe-fetch'; +import { isPrivateIp } from '../../../../lib/shared/net/private-ip'; +import { normalizeCatalogPayload } from '../../../../lib/shared/providers/catalog_normalize'; import type { ModelCatalogEntry, ProviderDefinition, -} from '../../../lib/shared/schemas/providers'; -import { checkProviderHostPolicy } from '../http/host_policy'; -import { safeFetch, SafeFetchError } from '../http/safe_fetch'; +} from '../../../../lib/shared/schemas/providers'; import { loadStaticCatalogs, type LoadSystemConfigOptions, diff --git a/services/platform/convex/lib/providers/chat_catalog.ts b/services/platform/backend/core/lib/providers/chat_catalog.ts similarity index 97% rename from services/platform/convex/lib/providers/chat_catalog.ts rename to services/platform/backend/core/lib/providers/chat_catalog.ts index c95d4d78f9..977645654b 100644 --- a/services/platform/convex/lib/providers/chat_catalog.ts +++ b/services/platform/backend/core/lib/providers/chat_catalog.ts @@ -19,7 +19,7 @@ * serving of a model over a sandbox-forcing subscription one. */ -import type { ModelCatalogEntry } from '../../../lib/shared/schemas/providers'; +import type { ModelCatalogEntry } from '../../../../lib/shared/schemas/providers'; import type { ActionCtx } from '../ctx'; import { getProviderCatalog } from './catalog_fetch'; import { credentialAuthFor } from './credential_auth'; diff --git a/services/platform/convex/lib/providers/credential_auth.ts b/services/platform/backend/core/lib/providers/credential_auth.ts similarity index 91% rename from services/platform/convex/lib/providers/credential_auth.ts rename to services/platform/backend/core/lib/providers/credential_auth.ts index c3bc09d1fd..f447700f82 100644 --- a/services/platform/convex/lib/providers/credential_auth.ts +++ b/services/platform/backend/core/lib/providers/credential_auth.ts @@ -9,11 +9,11 @@ * Layer A in spirit: pure data in, pure data out — no Convex imports. */ -import type { CredentialAuth } from '../../../lib/shared/providers/resolve_execution'; +import type { CredentialAuth } from '../../../../lib/shared/providers/resolve_execution'; import type { ProviderAuthMethodName, ProviderDefinition, -} from '../../../lib/shared/schemas/providers'; +} from '../../../../lib/shared/schemas/providers'; /** * Returns `null` when the provider does not offer the method the credential diff --git a/services/platform/convex/lib/providers/direct_credential.ts b/services/platform/backend/core/lib/providers/direct_credential.ts similarity index 100% rename from services/platform/convex/lib/providers/direct_credential.ts rename to services/platform/backend/core/lib/providers/direct_credential.ts diff --git a/services/platform/convex/lib/providers/harness_status.test.ts b/services/platform/backend/core/lib/providers/harness_status.test.ts similarity index 100% rename from services/platform/convex/lib/providers/harness_status.test.ts rename to services/platform/backend/core/lib/providers/harness_status.test.ts diff --git a/services/platform/convex/lib/providers/harness_status.ts b/services/platform/backend/core/lib/providers/harness_status.ts similarity index 97% rename from services/platform/convex/lib/providers/harness_status.ts rename to services/platform/backend/core/lib/providers/harness_status.ts index ecadfc83ad..c0fa33ca1f 100644 --- a/services/platform/convex/lib/providers/harness_status.ts +++ b/services/platform/backend/core/lib/providers/harness_status.ts @@ -25,11 +25,11 @@ import { buildHarnessTable, resolveExecution, type CredentialAuth, -} from '../../../lib/shared/providers/resolve_execution'; +} from '../../../../lib/shared/providers/resolve_execution'; import type { HarnessDefinition, ModelCatalogEntry, -} from '../../../lib/shared/schemas/providers'; +} from '../../../../lib/shared/schemas/providers'; export type HarnessManagedStatus = | { diff --git a/services/platform/convex/lib/providers/load_system_config.test.ts b/services/platform/backend/core/lib/providers/load_system_config.test.ts similarity index 99% rename from services/platform/convex/lib/providers/load_system_config.test.ts rename to services/platform/backend/core/lib/providers/load_system_config.test.ts index bec60f1a4b..fdd166cadb 100644 --- a/services/platform/convex/lib/providers/load_system_config.test.ts +++ b/services/platform/backend/core/lib/providers/load_system_config.test.ts @@ -6,7 +6,7 @@ import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { buildHarnessTable } from '../../../lib/shared/providers/resolve_execution'; +import { buildHarnessTable } from '../../../../lib/shared/providers/resolve_execution'; import { loadHarnesses, loadProviderDefinitions, diff --git a/services/platform/convex/lib/providers/load_system_config.ts b/services/platform/backend/core/lib/providers/load_system_config.ts similarity index 98% rename from services/platform/convex/lib/providers/load_system_config.ts rename to services/platform/backend/core/lib/providers/load_system_config.ts index ee8b21aa35..a9a0ddc4a4 100644 --- a/services/platform/convex/lib/providers/load_system_config.ts +++ b/services/platform/backend/core/lib/providers/load_system_config.ts @@ -37,8 +37,8 @@ import path from 'node:path'; import { z } from 'zod/v4'; -import { parseYaml } from '../../../lib/shared/config/yaml'; -import { formatZodError } from '../../../lib/shared/schemas/format-error'; +import { parseYaml } from '../../../../lib/shared/config/yaml'; +import { formatZodError } from '../../../../lib/shared/schemas/format-error'; import { harnessDefinitionSchema, modelCatalogFileSchema, @@ -46,7 +46,7 @@ import { type HarnessDefinition, type ModelCatalogEntry, type ProviderDefinition, -} from '../../../lib/shared/schemas/providers'; +} from '../../../../lib/shared/schemas/providers'; /** Repo-relative location of the shipped system config tree. */ const REPO_SYSTEM_ROOT = ['configs', 'platform', 'system'] as const; diff --git a/services/platform/convex/lib/providers/org_providers.test.ts b/services/platform/backend/core/lib/providers/org_providers.test.ts similarity index 100% rename from services/platform/convex/lib/providers/org_providers.test.ts rename to services/platform/backend/core/lib/providers/org_providers.test.ts diff --git a/services/platform/convex/lib/providers/org_providers.ts b/services/platform/backend/core/lib/providers/org_providers.ts similarity index 96% rename from services/platform/convex/lib/providers/org_providers.ts rename to services/platform/backend/core/lib/providers/org_providers.ts index f60a7ff4db..f781110004 100644 --- a/services/platform/convex/lib/providers/org_providers.ts +++ b/services/platform/backend/core/lib/providers/org_providers.ts @@ -32,12 +32,12 @@ import { readdirSync, readFileSync } from 'node:fs'; import path from 'node:path'; -import { parseYaml } from '../../../lib/shared/config/yaml'; -import { zodErrorMessage } from '../../../lib/shared/schemas/format-error'; +import { parseYaml } from '../../../../lib/shared/config/yaml'; +import { zodErrorMessage } from '../../../../lib/shared/schemas/format-error'; import { providerDefinitionSchema, type ProviderDefinition, -} from '../../../lib/shared/schemas/providers'; +} from '../../../../lib/shared/schemas/providers'; import { errnoCode, getConfigRoot, validateOrgSlug } from '../file_io'; import { orgSlugFromId } from '../helpers/org_slug'; import { diff --git a/services/platform/convex/lib/providers/resolve_chat_model.test.ts b/services/platform/backend/core/lib/providers/resolve_chat_model.test.ts similarity index 99% rename from services/platform/convex/lib/providers/resolve_chat_model.test.ts rename to services/platform/backend/core/lib/providers/resolve_chat_model.test.ts index 2c31bb1cba..9c7f034c9e 100644 --- a/services/platform/convex/lib/providers/resolve_chat_model.test.ts +++ b/services/platform/backend/core/lib/providers/resolve_chat_model.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import type { ModelCatalogEntry } from '../../../lib/shared/schemas/providers'; +import type { ModelCatalogEntry } from '../../../../lib/shared/schemas/providers'; import type { ActionCtx } from '../ctx'; import { walkChatCatalog, type ChatCatalogHit } from './chat_catalog'; import { resolveChatModel } from './resolve_chat_model'; diff --git a/services/platform/convex/lib/providers/resolve_chat_model.ts b/services/platform/backend/core/lib/providers/resolve_chat_model.ts similarity index 98% rename from services/platform/convex/lib/providers/resolve_chat_model.ts rename to services/platform/backend/core/lib/providers/resolve_chat_model.ts index 5f31758edb..5e3b7026d0 100644 --- a/services/platform/convex/lib/providers/resolve_chat_model.ts +++ b/services/platform/backend/core/lib/providers/resolve_chat_model.ts @@ -38,8 +38,8 @@ import { eligibleChatCandidates, type ChatAutoRefusal, type ModelBand, -} from '../../../lib/chat'; -import type { ModelCatalogEntry } from '../../../lib/shared/schemas/providers'; +} from '../../../../lib/chat'; +import type { ModelCatalogEntry } from '../../../../lib/shared/schemas/providers'; import type { ActionCtx } from '../ctx'; import { internal } from '../handler_names'; import { walkChatCatalog } from './chat_catalog'; diff --git a/services/platform/convex/lib/providers/resolve_transcription_model.ts b/services/platform/backend/core/lib/providers/resolve_transcription_model.ts similarity index 97% rename from services/platform/convex/lib/providers/resolve_transcription_model.ts rename to services/platform/backend/core/lib/providers/resolve_transcription_model.ts index 5f4fc9c2c6..7eaa94bdfa 100644 --- a/services/platform/convex/lib/providers/resolve_transcription_model.ts +++ b/services/platform/backend/core/lib/providers/resolve_transcription_model.ts @@ -13,7 +13,7 @@ * has no transcription endpoint at all. */ -import { AppError } from '../../../lib/shared/errors/app-error'; +import { AppError } from '../../../../lib/shared/errors/app-error'; import { resolveProviderCredential } from '../../provider_credentials/resolve_credential'; import type { ActionCtx } from '../ctx'; import { getProviderCatalog } from './catalog_fetch'; diff --git a/services/platform/convex/lib/providers/resolve_tts_model.test.ts b/services/platform/backend/core/lib/providers/resolve_tts_model.test.ts similarity index 98% rename from services/platform/convex/lib/providers/resolve_tts_model.test.ts rename to services/platform/backend/core/lib/providers/resolve_tts_model.test.ts index 010014dcff..1c194aa345 100644 --- a/services/platform/convex/lib/providers/resolve_tts_model.test.ts +++ b/services/platform/backend/core/lib/providers/resolve_tts_model.test.ts @@ -9,7 +9,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { AppError } from '../../../lib/shared/errors/app-error'; +import { AppError } from '../../../../lib/shared/errors/app-error'; import type { ActionCtx } from '../ctx'; const resolveProvidersMock = vi.fn(); diff --git a/services/platform/convex/lib/providers/resolve_tts_model.ts b/services/platform/backend/core/lib/providers/resolve_tts_model.ts similarity index 96% rename from services/platform/convex/lib/providers/resolve_tts_model.ts rename to services/platform/backend/core/lib/providers/resolve_tts_model.ts index 1f40b34444..84b0ed3827 100644 --- a/services/platform/convex/lib/providers/resolve_tts_model.ts +++ b/services/platform/backend/core/lib/providers/resolve_tts_model.ts @@ -11,8 +11,8 @@ * member on the chunk rows, so no free text ever leaves here. */ -import { AppError } from '../../../lib/shared/errors/app-error'; -import type { AudioFormat } from '../../../lib/shared/schemas/providers'; +import { AppError } from '../../../../lib/shared/errors/app-error'; +import type { AudioFormat } from '../../../../lib/shared/schemas/providers'; import { resolveProviderCredential } from '../../provider_credentials/resolve_credential'; import type { ActionCtx } from '../ctx'; import { getProviderCatalog } from './catalog_fetch'; diff --git a/services/platform/convex/lib/providers/resolve_vision_model.test.ts b/services/platform/backend/core/lib/providers/resolve_vision_model.test.ts similarity index 99% rename from services/platform/convex/lib/providers/resolve_vision_model.test.ts rename to services/platform/backend/core/lib/providers/resolve_vision_model.test.ts index 34190820c3..69c30dd335 100644 --- a/services/platform/convex/lib/providers/resolve_vision_model.test.ts +++ b/services/platform/backend/core/lib/providers/resolve_vision_model.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import type { ProviderDefinition } from '../../../lib/shared/schemas/providers'; +import type { ProviderDefinition } from '../../../../lib/shared/schemas/providers'; import type { ActionCtx } from '../ctx'; import { getProviderCatalog } from './catalog_fetch'; import { resolveProvidersForOrgId } from './org_providers'; diff --git a/services/platform/convex/lib/providers/resolve_vision_model.ts b/services/platform/backend/core/lib/providers/resolve_vision_model.ts similarity index 97% rename from services/platform/convex/lib/providers/resolve_vision_model.ts rename to services/platform/backend/core/lib/providers/resolve_vision_model.ts index 601b799c6b..533dc4a85d 100644 --- a/services/platform/convex/lib/providers/resolve_vision_model.ts +++ b/services/platform/backend/core/lib/providers/resolve_vision_model.ts @@ -35,9 +35,9 @@ * free-tier 401 storms, each observed live). */ -import { visionModelConfigSchema } from '../../../lib/shared/schemas/governance'; -import type { ModelCatalogEntry } from '../../../lib/shared/schemas/providers'; -import { modelIdsEquivalent } from '../../../lib/shared/utils/model-ref'; +import { visionModelConfigSchema } from '../../../../lib/shared/schemas/governance'; +import type { ModelCatalogEntry } from '../../../../lib/shared/schemas/providers'; +import { modelIdsEquivalent } from '../../../../lib/shared/utils/model-ref'; import type { ActionCtx } from '../ctx'; import { internal } from '../handler_names'; import { getProviderCatalog } from './catalog_fetch'; diff --git a/services/platform/convex/lib/rest/helpers.test.ts b/services/platform/backend/core/lib/rest/helpers.test.ts similarity index 100% rename from services/platform/convex/lib/rest/helpers.test.ts rename to services/platform/backend/core/lib/rest/helpers.test.ts diff --git a/services/platform/convex/lib/rest/helpers.ts b/services/platform/backend/core/lib/rest/helpers.ts similarity index 99% rename from services/platform/convex/lib/rest/helpers.ts rename to services/platform/backend/core/lib/rest/helpers.ts index e6c54fb975..e8cab28924 100644 --- a/services/platform/convex/lib/rest/helpers.ts +++ b/services/platform/backend/core/lib/rest/helpers.ts @@ -5,8 +5,8 @@ * URL parsing, and CORS handling used across all /api/v1/* REST routes. */ -import { defineAbilityFor } from '../../../lib/permissions/ability'; -import { AppError } from '../../../lib/shared/errors/app-error'; +import { defineAbilityFor } from '../../../../lib/permissions/ability'; +import { AppError } from '../../../../lib/shared/errors/app-error'; import { internal } from '../handler_names'; // --------------------------------------------------------------------------- // Types diff --git a/services/platform/convex/lib/rls/MEMBERSHIP_MIRROR_DESIGN.md b/services/platform/backend/core/lib/rls/MEMBERSHIP_MIRROR_DESIGN.md similarity index 100% rename from services/platform/convex/lib/rls/MEMBERSHIP_MIRROR_DESIGN.md rename to services/platform/backend/core/lib/rls/MEMBERSHIP_MIRROR_DESIGN.md diff --git a/services/platform/convex/lib/rls/auth/get_auth_user_identity.test.ts b/services/platform/backend/core/lib/rls/auth/get_auth_user_identity.test.ts similarity index 100% rename from services/platform/convex/lib/rls/auth/get_auth_user_identity.test.ts rename to services/platform/backend/core/lib/rls/auth/get_auth_user_identity.test.ts diff --git a/services/platform/convex/lib/rls/auth/get_auth_user_identity.ts b/services/platform/backend/core/lib/rls/auth/get_auth_user_identity.ts similarity index 100% rename from services/platform/convex/lib/rls/auth/get_auth_user_identity.ts rename to services/platform/backend/core/lib/rls/auth/get_auth_user_identity.ts diff --git a/services/platform/convex/lib/rls/auth/get_authenticated_user.ts b/services/platform/backend/core/lib/rls/auth/get_authenticated_user.ts similarity index 100% rename from services/platform/convex/lib/rls/auth/get_authenticated_user.ts rename to services/platform/backend/core/lib/rls/auth/get_authenticated_user.ts diff --git a/services/platform/convex/lib/rls/auth/get_trusted_auth_data.ts b/services/platform/backend/core/lib/rls/auth/get_trusted_auth_data.ts similarity index 96% rename from services/platform/convex/lib/rls/auth/get_trusted_auth_data.ts rename to services/platform/backend/core/lib/rls/auth/get_trusted_auth_data.ts index 52d13a34f1..8a0fd802b0 100644 --- a/services/platform/convex/lib/rls/auth/get_trusted_auth_data.ts +++ b/services/platform/backend/core/lib/rls/auth/get_trusted_auth_data.ts @@ -5,7 +5,7 @@ * and included in the JWT claims. This helper extracts and parses that data. */ -import { isRecord, getString } from '../../../../lib/utils/type-utils'; +import { isRecord, getString } from '../../../../../lib/utils/type-utils'; import type { QueryCtx, MutationCtx, ActionCtx } from '../../ctx'; export interface TrustedAuthData { diff --git a/services/platform/convex/lib/rls/auth/require_authenticated_user.ts b/services/platform/backend/core/lib/rls/auth/require_authenticated_user.ts similarity index 100% rename from services/platform/convex/lib/rls/auth/require_authenticated_user.ts rename to services/platform/backend/core/lib/rls/auth/require_authenticated_user.ts diff --git a/services/platform/convex/lib/rls/errors.ts b/services/platform/backend/core/lib/rls/errors.ts similarity index 96% rename from services/platform/convex/lib/rls/errors.ts rename to services/platform/backend/core/lib/rls/errors.ts index 444959d0a3..ddad90d197 100644 --- a/services/platform/convex/lib/rls/errors.ts +++ b/services/platform/backend/core/lib/rls/errors.ts @@ -15,7 +15,7 @@ * logs stay readable (the wire format serializes `data`, never `message`). */ -import { AppError } from '../../../lib/shared/errors/app-error'; +import { AppError } from '../../../../lib/shared/errors/app-error'; /** * Base RLS error class diff --git a/services/platform/convex/lib/rls/helpers/access_control.test.ts b/services/platform/backend/core/lib/rls/helpers/access_control.test.ts similarity index 100% rename from services/platform/convex/lib/rls/helpers/access_control.test.ts rename to services/platform/backend/core/lib/rls/helpers/access_control.test.ts diff --git a/services/platform/convex/lib/rls/helpers/access_control.ts b/services/platform/backend/core/lib/rls/helpers/access_control.ts similarity index 100% rename from services/platform/convex/lib/rls/helpers/access_control.ts rename to services/platform/backend/core/lib/rls/helpers/access_control.ts diff --git a/services/platform/convex/lib/rls/helpers/agent_read_access.ts b/services/platform/backend/core/lib/rls/helpers/agent_read_access.ts similarity index 100% rename from services/platform/convex/lib/rls/helpers/agent_read_access.ts rename to services/platform/backend/core/lib/rls/helpers/agent_read_access.ts diff --git a/services/platform/convex/lib/rls/helpers/conversation_assignment.test.ts b/services/platform/backend/core/lib/rls/helpers/conversation_assignment.test.ts similarity index 100% rename from services/platform/convex/lib/rls/helpers/conversation_assignment.test.ts rename to services/platform/backend/core/lib/rls/helpers/conversation_assignment.test.ts diff --git a/services/platform/convex/lib/rls/helpers/conversation_assignment.ts b/services/platform/backend/core/lib/rls/helpers/conversation_assignment.ts similarity index 100% rename from services/platform/convex/lib/rls/helpers/conversation_assignment.ts rename to services/platform/backend/core/lib/rls/helpers/conversation_assignment.ts diff --git a/services/platform/convex/lib/rls/helpers/role_helpers.test.ts b/services/platform/backend/core/lib/rls/helpers/role_helpers.test.ts similarity index 100% rename from services/platform/convex/lib/rls/helpers/role_helpers.test.ts rename to services/platform/backend/core/lib/rls/helpers/role_helpers.test.ts diff --git a/services/platform/convex/lib/rls/helpers/role_helpers.ts b/services/platform/backend/core/lib/rls/helpers/role_helpers.ts similarity index 100% rename from services/platform/convex/lib/rls/helpers/role_helpers.ts rename to services/platform/backend/core/lib/rls/helpers/role_helpers.ts diff --git a/services/platform/convex/lib/rls/organization/get_organization_member.test.ts b/services/platform/backend/core/lib/rls/organization/get_organization_member.test.ts similarity index 100% rename from services/platform/convex/lib/rls/organization/get_organization_member.test.ts rename to services/platform/backend/core/lib/rls/organization/get_organization_member.test.ts diff --git a/services/platform/convex/lib/rls/organization/get_organization_member.ts b/services/platform/backend/core/lib/rls/organization/get_organization_member.ts similarity index 100% rename from services/platform/convex/lib/rls/organization/get_organization_member.ts rename to services/platform/backend/core/lib/rls/organization/get_organization_member.ts diff --git a/services/platform/convex/lib/rls/organization/get_user_organizations.test.ts b/services/platform/backend/core/lib/rls/organization/get_user_organizations.test.ts similarity index 100% rename from services/platform/convex/lib/rls/organization/get_user_organizations.test.ts rename to services/platform/backend/core/lib/rls/organization/get_user_organizations.test.ts diff --git a/services/platform/convex/lib/rls/organization/get_user_organizations.ts b/services/platform/backend/core/lib/rls/organization/get_user_organizations.ts similarity index 98% rename from services/platform/convex/lib/rls/organization/get_user_organizations.ts rename to services/platform/backend/core/lib/rls/organization/get_user_organizations.ts index c32f11ada4..2bbc2f786b 100644 --- a/services/platform/convex/lib/rls/organization/get_user_organizations.ts +++ b/services/platform/backend/core/lib/rls/organization/get_user_organizations.ts @@ -2,7 +2,7 @@ * Get all organizations user has access to from Better Auth */ -import type { MemberRole } from '../../../../lib/shared/schemas/organizations'; +import type { MemberRole } from '../../../../../lib/shared/schemas/organizations'; import type { QueryCtx } from '../../ctx'; import { components } from '../../handler_names'; import { getTrustedAuthData } from '../auth/get_trusted_auth_data'; diff --git a/services/platform/convex/lib/rls/types.ts b/services/platform/backend/core/lib/rls/types.ts similarity index 100% rename from services/platform/convex/lib/rls/types.ts rename to services/platform/backend/core/lib/rls/types.ts diff --git a/services/platform/convex/lib/rows.ts b/services/platform/backend/core/lib/rows.ts similarity index 100% rename from services/platform/convex/lib/rows.ts rename to services/platform/backend/core/lib/rows.ts diff --git a/services/platform/convex/lib/safe_path_segment.ts b/services/platform/backend/core/lib/safe_path_segment.ts similarity index 100% rename from services/platform/convex/lib/safe_path_segment.ts rename to services/platform/backend/core/lib/safe_path_segment.ts diff --git a/services/platform/convex/lib/search/index.ts b/services/platform/backend/core/lib/search/index.ts similarity index 100% rename from services/platform/convex/lib/search/index.ts rename to services/platform/backend/core/lib/search/index.ts diff --git a/services/platform/convex/lib/search/listing_intent.test.ts b/services/platform/backend/core/lib/search/listing_intent.test.ts similarity index 100% rename from services/platform/convex/lib/search/listing_intent.test.ts rename to services/platform/backend/core/lib/search/listing_intent.test.ts diff --git a/services/platform/convex/lib/search/listing_intent.ts b/services/platform/backend/core/lib/search/listing_intent.ts similarity index 98% rename from services/platform/convex/lib/search/listing_intent.ts rename to services/platform/backend/core/lib/search/listing_intent.ts index 5e0b0a253c..6e0d09bde4 100644 --- a/services/platform/convex/lib/search/listing_intent.ts +++ b/services/platform/backend/core/lib/search/listing_intent.ts @@ -22,7 +22,10 @@ * it names. */ -import type { RagSearchKind, RagSearchStatus } from '../../../lib/chat/tools'; +import type { + RagSearchKind, + RagSearchStatus, +} from '../../../../lib/chat/tools'; import { STOPWORDS } from './relevance'; export interface ListingIntent { diff --git a/services/platform/convex/lib/search/relevance.test.ts b/services/platform/backend/core/lib/search/relevance.test.ts similarity index 100% rename from services/platform/convex/lib/search/relevance.test.ts rename to services/platform/backend/core/lib/search/relevance.test.ts diff --git a/services/platform/convex/lib/search/relevance.ts b/services/platform/backend/core/lib/search/relevance.ts similarity index 100% rename from services/platform/convex/lib/search/relevance.ts rename to services/platform/backend/core/lib/search/relevance.ts diff --git a/services/platform/convex/lib/search/run_entity_search.ts b/services/platform/backend/core/lib/search/run_entity_search.ts similarity index 100% rename from services/platform/convex/lib/search/run_entity_search.ts rename to services/platform/backend/core/lib/search/run_entity_search.ts diff --git a/services/platform/convex/lib/search/scoped_substring_search.ts b/services/platform/backend/core/lib/search/scoped_substring_search.ts similarity index 100% rename from services/platform/convex/lib/search/scoped_substring_search.ts rename to services/platform/backend/core/lib/search/scoped_substring_search.ts diff --git a/services/platform/convex/lib/search/strategies/contacts.ts b/services/platform/backend/core/lib/search/strategies/contacts.ts similarity index 100% rename from services/platform/convex/lib/search/strategies/contacts.ts rename to services/platform/backend/core/lib/search/strategies/contacts.ts diff --git a/services/platform/convex/lib/search/strategies/documents.ts b/services/platform/backend/core/lib/search/strategies/documents.ts similarity index 100% rename from services/platform/convex/lib/search/strategies/documents.ts rename to services/platform/backend/core/lib/search/strategies/documents.ts diff --git a/services/platform/convex/lib/search/strategies/projects.ts b/services/platform/backend/core/lib/search/strategies/projects.ts similarity index 100% rename from services/platform/convex/lib/search/strategies/projects.ts rename to services/platform/backend/core/lib/search/strategies/projects.ts diff --git a/services/platform/convex/lib/search/strategies/tasks.ts b/services/platform/backend/core/lib/search/strategies/tasks.ts similarity index 100% rename from services/platform/convex/lib/search/strategies/tasks.ts rename to services/platform/backend/core/lib/search/strategies/tasks.ts diff --git a/services/platform/convex/lib/search/types.ts b/services/platform/backend/core/lib/search/types.ts similarity index 100% rename from services/platform/convex/lib/search/types.ts rename to services/platform/backend/core/lib/search/types.ts diff --git a/services/platform/convex/lib/secret_box.ts b/services/platform/backend/core/lib/secret_box.ts similarity index 100% rename from services/platform/convex/lib/secret_box.ts rename to services/platform/backend/core/lib/secret_box.ts diff --git a/services/platform/convex/lib/sops.ts b/services/platform/backend/core/lib/sops.ts similarity index 100% rename from services/platform/convex/lib/sops.ts rename to services/platform/backend/core/lib/sops.ts diff --git a/services/platform/convex/lib/storage/blob_access.ts b/services/platform/backend/core/lib/storage/blob_access.ts similarity index 100% rename from services/platform/convex/lib/storage/blob_access.ts rename to services/platform/backend/core/lib/storage/blob_access.ts diff --git a/services/platform/convex/lib/storage/blob_delete.ts b/services/platform/backend/core/lib/storage/blob_delete.ts similarity index 100% rename from services/platform/convex/lib/storage/blob_delete.ts rename to services/platform/backend/core/lib/storage/blob_delete.ts diff --git a/services/platform/convex/lib/storage/blob_ref.test.ts b/services/platform/backend/core/lib/storage/blob_ref.test.ts similarity index 100% rename from services/platform/convex/lib/storage/blob_ref.test.ts rename to services/platform/backend/core/lib/storage/blob_ref.test.ts diff --git a/services/platform/convex/lib/storage/blob_ref.ts b/services/platform/backend/core/lib/storage/blob_ref.ts similarity index 100% rename from services/platform/convex/lib/storage/blob_ref.ts rename to services/platform/backend/core/lib/storage/blob_ref.ts diff --git a/services/platform/convex/lib/storage/browser_facing.test.ts b/services/platform/backend/core/lib/storage/browser_facing.test.ts similarity index 100% rename from services/platform/convex/lib/storage/browser_facing.test.ts rename to services/platform/backend/core/lib/storage/browser_facing.test.ts diff --git a/services/platform/convex/lib/storage/object_store.ts b/services/platform/backend/core/lib/storage/object_store.ts similarity index 100% rename from services/platform/convex/lib/storage/object_store.ts rename to services/platform/backend/core/lib/storage/object_store.ts diff --git a/services/platform/convex/lib/storage/sandbox_stage_token.ts b/services/platform/backend/core/lib/storage/sandbox_stage_token.ts similarity index 100% rename from services/platform/convex/lib/storage/sandbox_stage_token.ts rename to services/platform/backend/core/lib/storage/sandbox_stage_token.ts diff --git a/services/platform/convex/lib/team_access.ts b/services/platform/backend/core/lib/team_access.ts similarity index 100% rename from services/platform/convex/lib/team_access.ts rename to services/platform/backend/core/lib/team_access.ts diff --git a/services/platform/convex/lib/types/pdfjs_worker.d.ts b/services/platform/backend/core/lib/types/pdfjs_worker.d.ts similarity index 100% rename from services/platform/convex/lib/types/pdfjs_worker.d.ts rename to services/platform/backend/core/lib/types/pdfjs_worker.d.ts diff --git a/services/platform/convex/lib/utils/client_ip.test.ts b/services/platform/backend/core/lib/utils/client_ip.test.ts similarity index 100% rename from services/platform/convex/lib/utils/client_ip.test.ts rename to services/platform/backend/core/lib/utils/client_ip.test.ts diff --git a/services/platform/convex/lib/utils/client_ip.ts b/services/platform/backend/core/lib/utils/client_ip.ts similarity index 100% rename from services/platform/convex/lib/utils/client_ip.ts rename to services/platform/backend/core/lib/utils/client_ip.ts diff --git a/services/platform/convex/lib/utils/sanitize_secrets.test.ts b/services/platform/backend/core/lib/utils/sanitize_secrets.test.ts similarity index 100% rename from services/platform/convex/lib/utils/sanitize_secrets.test.ts rename to services/platform/backend/core/lib/utils/sanitize_secrets.test.ts diff --git a/services/platform/convex/lib/utils/sanitize_secrets.ts b/services/platform/backend/core/lib/utils/sanitize_secrets.ts similarity index 100% rename from services/platform/convex/lib/utils/sanitize_secrets.ts rename to services/platform/backend/core/lib/utils/sanitize_secrets.ts diff --git a/services/platform/convex/login_attempts/helpers.test.ts b/services/platform/backend/core/login_attempts/helpers.test.ts similarity index 98% rename from services/platform/convex/login_attempts/helpers.test.ts rename to services/platform/backend/core/login_attempts/helpers.test.ts index 4860c5b0d9..e31f41134b 100644 --- a/services/platform/convex/login_attempts/helpers.test.ts +++ b/services/platform/backend/core/login_attempts/helpers.test.ts @@ -5,7 +5,7 @@ import { DEFAULT_LOGIN_MAX_ATTEMPTS, DEFAULT_TRUSTED_PROXIES, type LoginPolicyConfig, -} from '../../lib/shared/schemas/governance'; +} from '../../../lib/shared/schemas/governance'; import { computeLockedUntil, DEFAULT_LOGIN_POLICY, diff --git a/services/platform/convex/login_attempts/helpers.ts b/services/platform/backend/core/login_attempts/helpers.ts similarity index 97% rename from services/platform/convex/login_attempts/helpers.ts rename to services/platform/backend/core/login_attempts/helpers.ts index c41abcd295..c3f3f2416c 100644 --- a/services/platform/convex/login_attempts/helpers.ts +++ b/services/platform/backend/core/login_attempts/helpers.ts @@ -4,7 +4,7 @@ import { DEFAULT_TRUSTED_PROXIES, loginPolicyConfigSchema, type LoginPolicyConfig, -} from '../../lib/shared/schemas/governance'; +} from '../../../lib/shared/schemas/governance'; export const DEFAULT_LOGIN_POLICY: LoginPolicyConfig = { enabled: true, diff --git a/services/platform/convex/members/mirror_sync.ts b/services/platform/backend/core/members/mirror_sync.ts similarity index 100% rename from services/platform/convex/members/mirror_sync.ts rename to services/platform/backend/core/members/mirror_sync.ts diff --git a/services/platform/convex/members/types.ts b/services/platform/backend/core/members/types.ts similarity index 100% rename from services/platform/convex/members/types.ts rename to services/platform/backend/core/members/types.ts diff --git a/services/platform/convex/node_only/sandbox/connectors_bridge.test.ts b/services/platform/backend/core/node_only/sandbox/connectors_bridge.test.ts similarity index 98% rename from services/platform/convex/node_only/sandbox/connectors_bridge.test.ts rename to services/platform/backend/core/node_only/sandbox/connectors_bridge.test.ts index 4657fc3d62..f4cf5dc838 100644 --- a/services/platform/convex/node_only/sandbox/connectors_bridge.test.ts +++ b/services/platform/backend/core/node_only/sandbox/connectors_bridge.test.ts @@ -9,7 +9,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; -import { AppError } from '../../../lib/shared/errors/app-error'; +import { AppError } from '../../../../lib/shared/errors/app-error'; type Dispatch = ( args: Record, diff --git a/services/platform/convex/node_only/sandbox/connectors_bridge.ts b/services/platform/backend/core/node_only/sandbox/connectors_bridge.ts similarity index 98% rename from services/platform/convex/node_only/sandbox/connectors_bridge.ts rename to services/platform/backend/core/node_only/sandbox/connectors_bridge.ts index bf4599532d..a0b008ce64 100644 --- a/services/platform/convex/node_only/sandbox/connectors_bridge.ts +++ b/services/platform/backend/core/node_only/sandbox/connectors_bridge.ts @@ -3,8 +3,8 @@ import { findConnector, loadConnectorDefinitions, -} from '../../../lib/connectors/catalog'; -import { AppError } from '../../../lib/shared/errors/app-error'; +} from '../../../../lib/connectors/catalog'; +import { AppError } from '../../../../lib/shared/errors/app-error'; /** One reason an connector (or call) cannot run, with guidance the agent * relays to the user verbatim. */ interface BridgeBlocker { diff --git a/services/platform/convex/node_only/sandbox/engine_exec_runner.test.ts b/services/platform/backend/core/node_only/sandbox/engine_exec_runner.test.ts similarity index 100% rename from services/platform/convex/node_only/sandbox/engine_exec_runner.test.ts rename to services/platform/backend/core/node_only/sandbox/engine_exec_runner.test.ts diff --git a/services/platform/convex/node_only/sandbox/engine_exec_runner.ts b/services/platform/backend/core/node_only/sandbox/engine_exec_runner.ts similarity index 95% rename from services/platform/convex/node_only/sandbox/engine_exec_runner.ts rename to services/platform/backend/core/node_only/sandbox/engine_exec_runner.ts index 107066ea2b..a1a1c86d31 100644 --- a/services/platform/convex/node_only/sandbox/engine_exec_runner.ts +++ b/services/platform/backend/core/node_only/sandbox/engine_exec_runner.ts @@ -20,12 +20,12 @@ import { randomUUID } from 'node:crypto'; -import type { CodeRunner } from '../../../lib/engine/core/runner'; +import type { CodeRunner } from '../../../../lib/engine/core/runner'; import { createSandboxExecRunner, createSessionTransport, type SandboxProgramRunner, -} from '../../../lib/engine/runners/sandbox-exec'; +} from '../../../../lib/engine/runners/sandbox-exec'; import { drainSessionExecResilient } from './helpers/session_client'; /** An out-of-process boundary has a real payload ceiling; a connector body's diff --git a/services/platform/convex/node_only/sandbox/gateway_provisioning.test.ts b/services/platform/backend/core/node_only/sandbox/gateway_provisioning.test.ts similarity index 100% rename from services/platform/convex/node_only/sandbox/gateway_provisioning.test.ts rename to services/platform/backend/core/node_only/sandbox/gateway_provisioning.test.ts diff --git a/services/platform/convex/node_only/sandbox/gateway_provisioning.ts b/services/platform/backend/core/node_only/sandbox/gateway_provisioning.ts similarity index 99% rename from services/platform/convex/node_only/sandbox/gateway_provisioning.ts rename to services/platform/backend/core/node_only/sandbox/gateway_provisioning.ts index f12aa3ac07..9eaded633e 100644 --- a/services/platform/convex/node_only/sandbox/gateway_provisioning.ts +++ b/services/platform/backend/core/node_only/sandbox/gateway_provisioning.ts @@ -26,7 +26,7 @@ * org is a routing preference. */ -import { AppError } from '../../../lib/shared/errors/app-error'; +import { AppError } from '../../../../lib/shared/errors/app-error'; import type { ActionCtx } from '../../lib/ctx'; import { internal } from '../../lib/handler_names'; import { getProviderCatalog } from '../../lib/providers/catalog_fetch'; diff --git a/services/platform/convex/node_only/sandbox/helpers/session_client.test.ts b/services/platform/backend/core/node_only/sandbox/helpers/session_client.test.ts similarity index 100% rename from services/platform/convex/node_only/sandbox/helpers/session_client.test.ts rename to services/platform/backend/core/node_only/sandbox/helpers/session_client.test.ts diff --git a/services/platform/convex/node_only/sandbox/helpers/session_client.ts b/services/platform/backend/core/node_only/sandbox/helpers/session_client.ts similarity index 100% rename from services/platform/convex/node_only/sandbox/helpers/session_client.ts rename to services/platform/backend/core/node_only/sandbox/helpers/session_client.ts diff --git a/services/platform/convex/node_only/sandbox/helpers/stage_url.ts b/services/platform/backend/core/node_only/sandbox/helpers/stage_url.ts similarity index 100% rename from services/platform/convex/node_only/sandbox/helpers/stage_url.ts rename to services/platform/backend/core/node_only/sandbox/helpers/stage_url.ts diff --git a/services/platform/convex/node_only/sandbox/llm_gateway_admin.test.ts b/services/platform/backend/core/node_only/sandbox/llm_gateway_admin.test.ts similarity index 100% rename from services/platform/convex/node_only/sandbox/llm_gateway_admin.test.ts rename to services/platform/backend/core/node_only/sandbox/llm_gateway_admin.test.ts diff --git a/services/platform/convex/node_only/sandbox/llm_gateway_admin.ts b/services/platform/backend/core/node_only/sandbox/llm_gateway_admin.ts similarity index 99% rename from services/platform/convex/node_only/sandbox/llm_gateway_admin.ts rename to services/platform/backend/core/node_only/sandbox/llm_gateway_admin.ts index 7a385dc13a..06135a94bb 100644 --- a/services/platform/convex/node_only/sandbox/llm_gateway_admin.ts +++ b/services/platform/backend/core/node_only/sandbox/llm_gateway_admin.ts @@ -33,7 +33,7 @@ import { createHash } from 'node:crypto'; -import { providerAttributionHeaders } from '../../../lib/shared/providers/attribution'; +import { providerAttributionHeaders } from '../../../../lib/shared/providers/attribution'; import { sanitizeError } from '../../lib/utils/sanitize_secrets'; /** diff --git a/services/platform/convex/node_only/sandbox/render_fetch.test.ts b/services/platform/backend/core/node_only/sandbox/render_fetch.test.ts similarity index 100% rename from services/platform/convex/node_only/sandbox/render_fetch.test.ts rename to services/platform/backend/core/node_only/sandbox/render_fetch.test.ts diff --git a/services/platform/convex/node_only/sandbox/render_fetch.ts b/services/platform/backend/core/node_only/sandbox/render_fetch.ts similarity index 100% rename from services/platform/convex/node_only/sandbox/render_fetch.ts rename to services/platform/backend/core/node_only/sandbox/render_fetch.ts diff --git a/services/platform/convex/node_only/sandbox/session_credentials.test.ts b/services/platform/backend/core/node_only/sandbox/session_credentials.test.ts similarity index 100% rename from services/platform/convex/node_only/sandbox/session_credentials.test.ts rename to services/platform/backend/core/node_only/sandbox/session_credentials.test.ts diff --git a/services/platform/convex/node_only/sandbox/session_credentials.ts b/services/platform/backend/core/node_only/sandbox/session_credentials.ts similarity index 100% rename from services/platform/convex/node_only/sandbox/session_credentials.ts rename to services/platform/backend/core/node_only/sandbox/session_credentials.ts diff --git a/services/platform/convex/node_only/sandbox/session_exec.test.ts b/services/platform/backend/core/node_only/sandbox/session_exec.test.ts similarity index 99% rename from services/platform/convex/node_only/sandbox/session_exec.test.ts rename to services/platform/backend/core/node_only/sandbox/session_exec.test.ts index fd9a63f980..4f084249fe 100644 --- a/services/platform/convex/node_only/sandbox/session_exec.test.ts +++ b/services/platform/backend/core/node_only/sandbox/session_exec.test.ts @@ -34,7 +34,7 @@ vi.mock('../../lib/helpers/org_slug', () => ({ orgSlugFromIdOrNull: (...args: unknown[]) => orgSlugFromIdOrNull(...args), })); -import { functionRefName } from '../../../lib/shared/handlers/function-refs'; +import { functionRefName } from '../../../../lib/shared/handlers/function-refs'; import { harvestSessionOutput, runStepsInSession } from './session_exec'; function execResult(over: Partial> = {}) { diff --git a/services/platform/convex/node_only/sandbox/session_exec.ts b/services/platform/backend/core/node_only/sandbox/session_exec.ts similarity index 100% rename from services/platform/convex/node_only/sandbox/session_exec.ts rename to services/platform/backend/core/node_only/sandbox/session_exec.ts diff --git a/services/platform/convex/node_only/sandbox/turn_equipment.ts b/services/platform/backend/core/node_only/sandbox/turn_equipment.ts similarity index 100% rename from services/platform/convex/node_only/sandbox/turn_equipment.ts rename to services/platform/backend/core/node_only/sandbox/turn_equipment.ts diff --git a/services/platform/convex/node_only/sandbox/workspace_domain_tools.ts b/services/platform/backend/core/node_only/sandbox/workspace_domain_tools.ts similarity index 99% rename from services/platform/convex/node_only/sandbox/workspace_domain_tools.ts rename to services/platform/backend/core/node_only/sandbox/workspace_domain_tools.ts index 710a66b793..b9d20e93bb 100644 --- a/services/platform/convex/node_only/sandbox/workspace_domain_tools.ts +++ b/services/platform/backend/core/node_only/sandbox/workspace_domain_tools.ts @@ -16,9 +16,9 @@ * domain's full audit/event trail via the internal mutation it calls. */ -import { AppError } from '../../../lib/shared/errors/app-error'; -import { extractExtension } from '../../../lib/shared/file-types'; -import { modelTimestamp } from '../../../lib/shared/model-timestamp'; +import { AppError } from '../../../../lib/shared/errors/app-error'; +import { extractExtension } from '../../../../lib/shared/file-types'; +import { modelTimestamp } from '../../../../lib/shared/model-timestamp'; import type { ActionCtx } from '../../lib/ctx'; import { internal } from '../../lib/handler_names'; import type { Doc, Id } from '../../lib/rows'; diff --git a/services/platform/convex/node_only/sandbox/workspace_tool_shared.ts b/services/platform/backend/core/node_only/sandbox/workspace_tool_shared.ts similarity index 100% rename from services/platform/convex/node_only/sandbox/workspace_tool_shared.ts rename to services/platform/backend/core/node_only/sandbox/workspace_tool_shared.ts diff --git a/services/platform/convex/node_only/sandbox/workspace_tools_bridge.test.ts b/services/platform/backend/core/node_only/sandbox/workspace_tools_bridge.test.ts similarity index 99% rename from services/platform/convex/node_only/sandbox/workspace_tools_bridge.test.ts rename to services/platform/backend/core/node_only/sandbox/workspace_tools_bridge.test.ts index 22247cf2ae..321fffad42 100644 --- a/services/platform/convex/node_only/sandbox/workspace_tools_bridge.test.ts +++ b/services/platform/backend/core/node_only/sandbox/workspace_tools_bridge.test.ts @@ -8,7 +8,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; -import { functionRefName } from '../../../lib/shared/handlers/function-refs'; +import { functionRefName } from '../../../../lib/shared/handlers/function-refs'; const searchKnowledgeMock = vi.fn(); vi.mock('../../knowledge/search', () => ({ diff --git a/services/platform/convex/node_only/sandbox/workspace_tools_bridge.ts b/services/platform/backend/core/node_only/sandbox/workspace_tools_bridge.ts similarity index 99% rename from services/platform/convex/node_only/sandbox/workspace_tools_bridge.ts rename to services/platform/backend/core/node_only/sandbox/workspace_tools_bridge.ts index aebf904255..f128f50c32 100644 --- a/services/platform/convex/node_only/sandbox/workspace_tools_bridge.ts +++ b/services/platform/backend/core/node_only/sandbox/workspace_tools_bridge.ts @@ -1,16 +1,17 @@ 'use node'; +import { wrapUntrusted } from '../../../../lib/chat/untrusted-content'; import { knowledgeScopeAllows, type KnowledgeAccessScope, -} from '../../../lib/knowledge/types'; -import { formatZodError } from '../../../lib/shared/schemas/format-error'; +} from '../../../../lib/knowledge/types'; +import { formatZodError } from '../../../../lib/shared/schemas/format-error'; import { MAX_OPTIONS_PER_QUESTION, MIN_OPTIONS_PER_QUESTION, questionSetSchema, type QuestionSet, -} from '../../../lib/shared/schemas/questions'; +} from '../../../../lib/shared/schemas/questions'; import { FETCH_WINDOW_CHARS, fetchDocumentByFileId, @@ -21,7 +22,6 @@ import { searchKnowledge } from '../../knowledge/search'; import type { ActionCtx } from '../../lib/ctx'; import { internal } from '../../lib/handler_names'; import { orgSlugFromId } from '../../lib/helpers/org_slug'; -import { wrapUntrusted } from '../../lib/untrusted_content'; import { ASK_HUMAN_TOOL, KNOWLEDGE_REFS_PER_CALL_CAP, diff --git a/services/platform/convex/notifications/actionable_email_connectors.ts b/services/platform/backend/core/notifications/actionable_email_connectors.ts similarity index 100% rename from services/platform/convex/notifications/actionable_email_connectors.ts rename to services/platform/backend/core/notifications/actionable_email_connectors.ts diff --git a/services/platform/convex/notifications/actionable_email_input.ts b/services/platform/backend/core/notifications/actionable_email_input.ts similarity index 100% rename from services/platform/convex/notifications/actionable_email_input.ts rename to services/platform/backend/core/notifications/actionable_email_input.ts diff --git a/services/platform/convex/notifications/actor_name.ts b/services/platform/backend/core/notifications/actor_name.ts similarity index 100% rename from services/platform/convex/notifications/actor_name.ts rename to services/platform/backend/core/notifications/actor_name.ts diff --git a/services/platform/convex/notifications/helpers.ts b/services/platform/backend/core/notifications/helpers.ts similarity index 100% rename from services/platform/convex/notifications/helpers.ts rename to services/platform/backend/core/notifications/helpers.ts diff --git a/services/platform/convex/notifications/notification_messages.test.ts b/services/platform/backend/core/notifications/notification_messages.test.ts similarity index 96% rename from services/platform/convex/notifications/notification_messages.test.ts rename to services/platform/backend/core/notifications/notification_messages.test.ts index 481b85cadd..fab3860326 100644 --- a/services/platform/convex/notifications/notification_messages.test.ts +++ b/services/platform/backend/core/notifications/notification_messages.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from 'vitest'; -import deMessages from '../../messages/de.yml'; -import enMessages from '../../messages/en.yml'; -import frMessages from '../../messages/fr.yml'; +import deMessages from '../../../messages/de.yml'; +import enMessages from '../../../messages/en.yml'; +import frMessages from '../../../messages/fr.yml'; import { ACTIONABLE_INBOX_KEYS, escapeSlackText, diff --git a/services/platform/convex/notifications/notification_messages.ts b/services/platform/backend/core/notifications/notification_messages.ts similarity index 99% rename from services/platform/convex/notifications/notification_messages.ts rename to services/platform/backend/core/notifications/notification_messages.ts index 54c691a13a..2c9acdc3e9 100644 --- a/services/platform/convex/notifications/notification_messages.ts +++ b/services/platform/backend/core/notifications/notification_messages.ts @@ -17,7 +17,7 @@ * markup intact. */ -import { interpolateTemplate } from '../../lib/shared/utils/interpolate'; +import { interpolateTemplate } from '../../../lib/shared/utils/interpolate'; export const SUPPORTED_NOTIFICATION_LOCALES = ['en', 'de', 'fr'] as const; export type NotificationLocale = diff --git a/services/platform/convex/notifications/personal_notification_url.ts b/services/platform/backend/core/notifications/personal_notification_url.ts similarity index 100% rename from services/platform/convex/notifications/personal_notification_url.ts rename to services/platform/backend/core/notifications/personal_notification_url.ts diff --git a/services/platform/convex/notifications/types.ts b/services/platform/backend/core/notifications/types.ts similarity index 100% rename from services/platform/convex/notifications/types.ts rename to services/platform/backend/core/notifications/types.ts diff --git a/services/platform/convex/object_storage/file_utils.ts b/services/platform/backend/core/object_storage/file_utils.ts similarity index 97% rename from services/platform/convex/object_storage/file_utils.ts rename to services/platform/backend/core/object_storage/file_utils.ts index 201eba4594..f2f3bfe06c 100644 --- a/services/platform/convex/object_storage/file_utils.ts +++ b/services/platform/backend/core/object_storage/file_utils.ts @@ -16,7 +16,7 @@ import path from 'node:path'; -import { zodErrorMessage } from '../../lib/shared/schemas/format-error'; +import { zodErrorMessage } from '../../../lib/shared/schemas/format-error'; import { OBJECT_STORAGE_CONFIG_DOMAIN, OBJECT_STORAGE_CONNECTION_KEY, @@ -24,7 +24,7 @@ import { objectStorageConnectionSecretsSchema, type ObjectStorageConnectionFile, type ObjectStorageConnectionSecrets, -} from '../../lib/shared/schemas/object_storage'; +} from '../../../lib/shared/schemas/object_storage'; import { errnoCode, getConfigRoot, diff --git a/services/platform/convex/onedrive/derive_sync_targets.test.ts b/services/platform/backend/core/onedrive/derive_sync_targets.test.ts similarity index 100% rename from services/platform/convex/onedrive/derive_sync_targets.test.ts rename to services/platform/backend/core/onedrive/derive_sync_targets.test.ts diff --git a/services/platform/convex/onedrive/derive_sync_targets.ts b/services/platform/backend/core/onedrive/derive_sync_targets.ts similarity index 100% rename from services/platform/convex/onedrive/derive_sync_targets.ts rename to services/platform/backend/core/onedrive/derive_sync_targets.ts diff --git a/services/platform/convex/onedrive/get_file_metadata.ts b/services/platform/backend/core/onedrive/get_file_metadata.ts similarity index 96% rename from services/platform/convex/onedrive/get_file_metadata.ts rename to services/platform/backend/core/onedrive/get_file_metadata.ts index db9cddb949..68c3b11fa1 100644 --- a/services/platform/convex/onedrive/get_file_metadata.ts +++ b/services/platform/backend/core/onedrive/get_file_metadata.ts @@ -1,4 +1,4 @@ -import { fetchJson } from '../../lib/utils/type-utils'; +import { fetchJson } from '../../../lib/utils/type-utils'; export interface FileMetadataResult { success: boolean; diff --git a/services/platform/convex/onedrive/import_files.test.ts b/services/platform/backend/core/onedrive/import_files.test.ts similarity index 100% rename from services/platform/convex/onedrive/import_files.test.ts rename to services/platform/backend/core/onedrive/import_files.test.ts diff --git a/services/platform/convex/onedrive/import_files.ts b/services/platform/backend/core/onedrive/import_files.ts similarity index 99% rename from services/platform/convex/onedrive/import_files.ts rename to services/platform/backend/core/onedrive/import_files.ts index 1e28e31aa1..a534ba85ae 100644 --- a/services/platform/convex/onedrive/import_files.ts +++ b/services/platform/backend/core/onedrive/import_files.ts @@ -2,7 +2,7 @@ * Import Files - Business logic for importing files from OneDrive/SharePoint */ -import { resolveFileType } from '../../lib/shared/file-types'; +import { resolveFileType } from '../../../lib/shared/file-types'; import type { Id } from '../lib/rows'; import type { BlobRef } from '../lib/storage/blob_ref'; import { deriveSyncTargets, type SyncTarget } from './derive_sync_targets'; diff --git a/services/platform/convex/onedrive/list_files.ts b/services/platform/backend/core/onedrive/list_files.ts similarity index 97% rename from services/platform/convex/onedrive/list_files.ts rename to services/platform/backend/core/onedrive/list_files.ts index a6109a358f..2ea14ebe66 100644 --- a/services/platform/convex/onedrive/list_files.ts +++ b/services/platform/backend/core/onedrive/list_files.ts @@ -1,4 +1,4 @@ -import { fetchJson } from '../../lib/utils/type-utils'; +import { fetchJson } from '../../../lib/utils/type-utils'; export interface OneDriveItem { id: string; diff --git a/services/platform/convex/onedrive/list_folder_contents.test.ts b/services/platform/backend/core/onedrive/list_folder_contents.test.ts similarity index 100% rename from services/platform/convex/onedrive/list_folder_contents.test.ts rename to services/platform/backend/core/onedrive/list_folder_contents.test.ts diff --git a/services/platform/convex/onedrive/list_folder_contents.ts b/services/platform/backend/core/onedrive/list_folder_contents.ts similarity index 98% rename from services/platform/convex/onedrive/list_folder_contents.ts rename to services/platform/backend/core/onedrive/list_folder_contents.ts index 155467fa53..24e5c50148 100644 --- a/services/platform/convex/onedrive/list_folder_contents.ts +++ b/services/platform/backend/core/onedrive/list_folder_contents.ts @@ -2,7 +2,7 @@ * List Folder Contents - Business logic for listing OneDrive folder contents */ -import { fetchJson } from '../../lib/utils/type-utils'; +import { fetchJson } from '../../../lib/utils/type-utils'; export interface FileItem { id: string; diff --git a/services/platform/convex/onedrive/list_sharepoint_drives.ts b/services/platform/backend/core/onedrive/list_sharepoint_drives.ts similarity index 97% rename from services/platform/convex/onedrive/list_sharepoint_drives.ts rename to services/platform/backend/core/onedrive/list_sharepoint_drives.ts index c565482724..aeae0867a6 100644 --- a/services/platform/convex/onedrive/list_sharepoint_drives.ts +++ b/services/platform/backend/core/onedrive/list_sharepoint_drives.ts @@ -1,4 +1,4 @@ -import { fetchJson } from '../../lib/utils/type-utils'; +import { fetchJson } from '../../../lib/utils/type-utils'; export interface SharePointDrive { id: string; diff --git a/services/platform/convex/onedrive/list_sharepoint_files.ts b/services/platform/backend/core/onedrive/list_sharepoint_files.ts similarity index 98% rename from services/platform/convex/onedrive/list_sharepoint_files.ts rename to services/platform/backend/core/onedrive/list_sharepoint_files.ts index 1a537c17b3..d67f80ab10 100644 --- a/services/platform/convex/onedrive/list_sharepoint_files.ts +++ b/services/platform/backend/core/onedrive/list_sharepoint_files.ts @@ -1,4 +1,4 @@ -import { fetchJson } from '../../lib/utils/type-utils'; +import { fetchJson } from '../../../lib/utils/type-utils'; export interface SharePointItem { id: string; diff --git a/services/platform/convex/onedrive/list_sharepoint_sites.ts b/services/platform/backend/core/onedrive/list_sharepoint_sites.ts similarity index 98% rename from services/platform/convex/onedrive/list_sharepoint_sites.ts rename to services/platform/backend/core/onedrive/list_sharepoint_sites.ts index c2f74334f8..cb5ba2a258 100644 --- a/services/platform/convex/onedrive/list_sharepoint_sites.ts +++ b/services/platform/backend/core/onedrive/list_sharepoint_sites.ts @@ -1,4 +1,4 @@ -import { fetchJson } from '../../lib/utils/type-utils'; +import { fetchJson } from '../../../lib/utils/type-utils'; export interface SharePointSite { id: string; diff --git a/services/platform/convex/onedrive/reconcile_folder_sync.test.ts b/services/platform/backend/core/onedrive/reconcile_folder_sync.test.ts similarity index 100% rename from services/platform/convex/onedrive/reconcile_folder_sync.test.ts rename to services/platform/backend/core/onedrive/reconcile_folder_sync.test.ts diff --git a/services/platform/convex/onedrive/reconcile_folder_sync.ts b/services/platform/backend/core/onedrive/reconcile_folder_sync.ts similarity index 100% rename from services/platform/convex/onedrive/reconcile_folder_sync.ts rename to services/platform/backend/core/onedrive/reconcile_folder_sync.ts diff --git a/services/platform/convex/onedrive/refresh_token.ts b/services/platform/backend/core/onedrive/refresh_token.ts similarity index 97% rename from services/platform/convex/onedrive/refresh_token.ts rename to services/platform/backend/core/onedrive/refresh_token.ts index 0d89a0a5ca..ff93eab617 100644 --- a/services/platform/convex/onedrive/refresh_token.ts +++ b/services/platform/backend/core/onedrive/refresh_token.ts @@ -2,7 +2,7 @@ * Refresh Token - Business logic for refreshing Microsoft OAuth tokens */ -import { fetchJson } from '../../lib/utils/type-utils'; +import { fetchJson } from '../../../lib/utils/type-utils'; export interface RefreshTokenResult { success: boolean; diff --git a/services/platform/convex/organizations/resolve_org_slug.test.ts b/services/platform/backend/core/organizations/resolve_org_slug.test.ts similarity index 100% rename from services/platform/convex/organizations/resolve_org_slug.test.ts rename to services/platform/backend/core/organizations/resolve_org_slug.test.ts diff --git a/services/platform/convex/organizations/resolve_org_slug.ts b/services/platform/backend/core/organizations/resolve_org_slug.ts similarity index 100% rename from services/platform/convex/organizations/resolve_org_slug.ts rename to services/platform/backend/core/organizations/resolve_org_slug.ts diff --git a/services/platform/convex/organizations/scaffold.ts b/services/platform/backend/core/organizations/scaffold.ts similarity index 99% rename from services/platform/convex/organizations/scaffold.ts rename to services/platform/backend/core/organizations/scaffold.ts index 3c65a4909b..01ebd99587 100644 --- a/services/platform/convex/organizations/scaffold.ts +++ b/services/platform/backend/core/organizations/scaffold.ts @@ -69,9 +69,9 @@ import type { z } from 'zod/v4'; import { CONFIG_DOMAINS, type ConfigDomain, -} from '../../lib/shared/config/registry'; -import { parseYaml } from '../../lib/shared/config/yaml'; -import { zodErrorMessage } from '../../lib/shared/schemas/format-error'; +} from '../../../lib/shared/config/registry'; +import { parseYaml } from '../../../lib/shared/config/yaml'; +import { zodErrorMessage } from '../../../lib/shared/schemas/format-error'; import { resolveBuiltinCatalogRoot } from '../lib/config_store/builtin_catalog'; import { resolveDomainDir } from '../lib/config_store/resolvers'; import { diff --git a/services/platform/convex/products/field_limits.test.ts b/services/platform/backend/core/products/field_limits.test.ts similarity index 96% rename from services/platform/convex/products/field_limits.test.ts rename to services/platform/backend/core/products/field_limits.test.ts index aeda626ba9..b36cfa75e3 100644 --- a/services/platform/convex/products/field_limits.test.ts +++ b/services/platform/backend/core/products/field_limits.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { PRODUCT_CATEGORY_MAX, PRODUCT_CURRENCY_MAX, diff --git a/services/platform/convex/products/field_limits.ts b/services/platform/backend/core/products/field_limits.ts similarity index 97% rename from services/platform/convex/products/field_limits.ts rename to services/platform/backend/core/products/field_limits.ts index 84f09e0917..45ba256417 100644 --- a/services/platform/convex/products/field_limits.ts +++ b/services/platform/backend/core/products/field_limits.ts @@ -8,7 +8,7 @@ * the create/edit dialog Zod schemas. */ -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; /** Maximum length of a product display name (characters). */ export const PRODUCT_NAME_MAX = 255; diff --git a/services/platform/convex/projects/access.test.ts b/services/platform/backend/core/projects/access.test.ts similarity index 100% rename from services/platform/convex/projects/access.test.ts rename to services/platform/backend/core/projects/access.test.ts diff --git a/services/platform/convex/projects/access.ts b/services/platform/backend/core/projects/access.ts similarity index 100% rename from services/platform/convex/projects/access.ts rename to services/platform/backend/core/projects/access.ts diff --git a/services/platform/convex/projects/audit_actions.ts b/services/platform/backend/core/projects/audit_actions.ts similarity index 100% rename from services/platform/convex/projects/audit_actions.ts rename to services/platform/backend/core/projects/audit_actions.ts diff --git a/services/platform/convex/projects/resolve_project_access.ts b/services/platform/backend/core/projects/resolve_project_access.ts similarity index 98% rename from services/platform/convex/projects/resolve_project_access.ts rename to services/platform/backend/core/projects/resolve_project_access.ts index df12588663..3f996a7957 100644 --- a/services/platform/convex/projects/resolve_project_access.ts +++ b/services/platform/backend/core/projects/resolve_project_access.ts @@ -5,7 +5,7 @@ * role, team ids — and fails CLOSED (no access) on any resolution error. */ -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import type { MutationCtx, QueryCtx } from '../lib/ctx'; import { getUserTeamIds } from '../lib/get_user_teams'; import { getOrganizationMember } from '../lib/rls/organization/get_organization_member'; diff --git a/services/platform/convex/provider_credentials/broker_pool.test.ts b/services/platform/backend/core/provider_credentials/broker_pool.test.ts similarity index 98% rename from services/platform/convex/provider_credentials/broker_pool.test.ts rename to services/platform/backend/core/provider_credentials/broker_pool.test.ts index fcfefb5318..84623c0298 100644 --- a/services/platform/convex/provider_credentials/broker_pool.test.ts +++ b/services/platform/backend/core/provider_credentials/broker_pool.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { BrokerResponseMapping } from '../../lib/shared/schemas/providers'; +import type { BrokerResponseMapping } from '../../../lib/shared/schemas/providers'; import { BrokerPoolError, buildBrokerAuthHeaders, diff --git a/services/platform/convex/provider_credentials/broker_pool.ts b/services/platform/backend/core/provider_credentials/broker_pool.ts similarity index 98% rename from services/platform/convex/provider_credentials/broker_pool.ts rename to services/platform/backend/core/provider_credentials/broker_pool.ts index 6b63155826..45b04c9ab5 100644 --- a/services/platform/convex/provider_credentials/broker_pool.ts +++ b/services/platform/backend/core/provider_credentials/broker_pool.ts @@ -16,9 +16,9 @@ import type { BrokerAuth, BrokerResponseMapping, BrokerSelection, -} from '../../lib/shared/schemas/providers'; -import dayjs from '../../lib/utils/date/dayjs-setup'; -import { isRecord } from '../../lib/utils/type-utils'; +} from '../../../lib/shared/schemas/providers'; +import dayjs from '../../../lib/utils/date/dayjs-setup'; +import { isRecord } from '../../../lib/utils/type-utils'; import { readJsonPath } from '../lib/json/json_path'; /** diff --git a/services/platform/convex/provider_credentials/masking.test.ts b/services/platform/backend/core/provider_credentials/masking.test.ts similarity index 100% rename from services/platform/convex/provider_credentials/masking.test.ts rename to services/platform/backend/core/provider_credentials/masking.test.ts diff --git a/services/platform/convex/provider_credentials/masking.ts b/services/platform/backend/core/provider_credentials/masking.ts similarity index 100% rename from services/platform/convex/provider_credentials/masking.ts rename to services/platform/backend/core/provider_credentials/masking.ts diff --git a/services/platform/convex/provider_credentials/resolve_credential.ts b/services/platform/backend/core/provider_credentials/resolve_credential.ts similarity index 98% rename from services/platform/convex/provider_credentials/resolve_credential.ts rename to services/platform/backend/core/provider_credentials/resolve_credential.ts index 44bdd02364..bf895d4181 100644 --- a/services/platform/convex/provider_credentials/resolve_credential.ts +++ b/services/platform/backend/core/provider_credentials/resolve_credential.ts @@ -26,16 +26,16 @@ import { z } from 'zod/v4'; -import { AppError } from '../../lib/shared/errors/app-error'; +import { safeFetch, SafeFetchError } from '../../../lib/net/safe-fetch'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { BROKER_SECRET_ENV_REGEX, brokerCredentialDataSchema, SECRETS_ENV_REGEX, type BrokerCredentialData, -} from '../../lib/shared/schemas/providers'; +} from '../../../lib/shared/schemas/providers'; import type { ActionCtx } from '../lib/ctx'; import { internal } from '../lib/handler_names'; -import { safeFetch, SafeFetchError } from '../lib/http/safe_fetch'; import type { Id } from '../lib/rows'; import { decryptSecret, diff --git a/services/platform/convex/provider_credentials/token_hash.test.ts b/services/platform/backend/core/provider_credentials/token_hash.test.ts similarity index 100% rename from services/platform/convex/provider_credentials/token_hash.test.ts rename to services/platform/backend/core/provider_credentials/token_hash.test.ts diff --git a/services/platform/convex/provider_credentials/token_hash.ts b/services/platform/backend/core/provider_credentials/token_hash.ts similarity index 100% rename from services/platform/convex/provider_credentials/token_hash.ts rename to services/platform/backend/core/provider_credentials/token_hash.ts diff --git a/services/platform/convex/provisioning/provision_default_automations.test.ts b/services/platform/backend/core/provisioning/provision_default_automations.test.ts similarity index 98% rename from services/platform/convex/provisioning/provision_default_automations.test.ts rename to services/platform/backend/core/provisioning/provision_default_automations.test.ts index 670455d0ae..069829df1e 100644 --- a/services/platform/convex/provisioning/provision_default_automations.test.ts +++ b/services/platform/backend/core/provisioning/provision_default_automations.test.ts @@ -20,7 +20,7 @@ import { loadSeedablePacks } from './provision_default_automations'; const REPO_CATALOG = path.resolve( path.dirname(fileURLToPath(import.meta.url)), - '../../../../configs/platform/custom', + '../../../../../configs/platform/custom', ); describe('loadSeedablePacks', () => { diff --git a/services/platform/convex/provisioning/provision_default_automations.ts b/services/platform/backend/core/provisioning/provision_default_automations.ts similarity index 96% rename from services/platform/convex/provisioning/provision_default_automations.ts rename to services/platform/backend/core/provisioning/provision_default_automations.ts index 0d67eedfd5..3a01d4742a 100644 --- a/services/platform/convex/provisioning/provision_default_automations.ts +++ b/services/platform/backend/core/provisioning/provision_default_automations.ts @@ -4,8 +4,8 @@ import { loadAutomationPacks, type AutomationTrigger, type LoadPacksOptions, -} from '../../lib/automations/packs'; -import type { Automation } from '../../lib/engine/core/types'; +} from '../../../lib/automations/packs'; +import type { Automation } from '../../../lib/engine/core/types'; import { resolveBuiltinCatalogRoot } from '../lib/config_store/builtin_catalog'; /** What one pack contributes to the seed batch. */ diff --git a/services/platform/convex/sandbox/agent_deadline.test.ts b/services/platform/backend/core/sandbox/agent_deadline.test.ts similarity index 100% rename from services/platform/convex/sandbox/agent_deadline.test.ts rename to services/platform/backend/core/sandbox/agent_deadline.test.ts diff --git a/services/platform/convex/sandbox/agent_deadline.ts b/services/platform/backend/core/sandbox/agent_deadline.ts similarity index 100% rename from services/platform/convex/sandbox/agent_deadline.ts rename to services/platform/backend/core/sandbox/agent_deadline.ts diff --git a/services/platform/convex/sandbox/quota_policy.ts b/services/platform/backend/core/sandbox/quota_policy.ts similarity index 96% rename from services/platform/convex/sandbox/quota_policy.ts rename to services/platform/backend/core/sandbox/quota_policy.ts index 1946e8d4ca..124e63488a 100644 --- a/services/platform/convex/sandbox/quota_policy.ts +++ b/services/platform/backend/core/sandbox/quota_policy.ts @@ -1,9 +1,9 @@ -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { DEFAULT_SANDBOX_QUOTA, sandboxQuotaConfigSchema, type SandboxQuotaConfig, -} from '../../lib/shared/schemas/governance'; +} from '../../../lib/shared/schemas/governance'; import { readConfigCacheRow } from '../lib/config_cache/read'; import type { DatabaseReader } from '../lib/ctx'; diff --git a/services/platform/convex/sandbox/session_constants.ts b/services/platform/backend/core/sandbox/session_constants.ts similarity index 100% rename from services/platform/convex/sandbox/session_constants.ts rename to services/platform/backend/core/sandbox/session_constants.ts diff --git a/services/platform/convex/sandbox/session_naming.test.ts b/services/platform/backend/core/sandbox/session_naming.test.ts similarity index 100% rename from services/platform/convex/sandbox/session_naming.test.ts rename to services/platform/backend/core/sandbox/session_naming.test.ts diff --git a/services/platform/convex/sandbox/session_naming.ts b/services/platform/backend/core/sandbox/session_naming.ts similarity index 100% rename from services/platform/convex/sandbox/session_naming.ts rename to services/platform/backend/core/sandbox/session_naming.ts diff --git a/services/platform/convex/sandbox/tool_names.test.ts b/services/platform/backend/core/sandbox/tool_names.test.ts similarity index 100% rename from services/platform/convex/sandbox/tool_names.test.ts rename to services/platform/backend/core/sandbox/tool_names.test.ts diff --git a/services/platform/convex/sandbox/tool_names.ts b/services/platform/backend/core/sandbox/tool_names.ts similarity index 100% rename from services/platform/convex/sandbox/tool_names.ts rename to services/platform/backend/core/sandbox/tool_names.ts diff --git a/services/platform/convex/sandbox/user_env_constants.test.ts b/services/platform/backend/core/sandbox/user_env_constants.test.ts similarity index 100% rename from services/platform/convex/sandbox/user_env_constants.test.ts rename to services/platform/backend/core/sandbox/user_env_constants.test.ts diff --git a/services/platform/convex/sandbox/user_env_constants.ts b/services/platform/backend/core/sandbox/user_env_constants.ts similarity index 100% rename from services/platform/convex/sandbox/user_env_constants.ts rename to services/platform/backend/core/sandbox/user_env_constants.ts diff --git a/services/platform/convex/sandbox/workspace_access.ts b/services/platform/backend/core/sandbox/workspace_access.ts similarity index 100% rename from services/platform/convex/sandbox/workspace_access.ts rename to services/platform/backend/core/sandbox/workspace_access.ts diff --git a/services/platform/convex/scim/data.ts b/services/platform/backend/core/scim/data.ts similarity index 100% rename from services/platform/convex/scim/data.ts rename to services/platform/backend/core/scim/data.ts diff --git a/services/platform/convex/scim/discovery.ts b/services/platform/backend/core/scim/discovery.ts similarity index 100% rename from services/platform/convex/scim/discovery.ts rename to services/platform/backend/core/scim/discovery.ts diff --git a/services/platform/convex/scim/helpers/crypto.ts b/services/platform/backend/core/scim/helpers/crypto.ts similarity index 100% rename from services/platform/convex/scim/helpers/crypto.ts rename to services/platform/backend/core/scim/helpers/crypto.ts diff --git a/services/platform/convex/scim/http_actions.ts b/services/platform/backend/core/scim/http_actions.ts similarity index 100% rename from services/platform/convex/scim/http_actions.ts rename to services/platform/backend/core/scim/http_actions.ts diff --git a/services/platform/convex/scim/internal_mutations.ts b/services/platform/backend/core/scim/internal_mutations.ts similarity index 100% rename from services/platform/convex/scim/internal_mutations.ts rename to services/platform/backend/core/scim/internal_mutations.ts diff --git a/services/platform/convex/scim/links.ts b/services/platform/backend/core/scim/links.ts similarity index 100% rename from services/platform/convex/scim/links.ts rename to services/platform/backend/core/scim/links.ts diff --git a/services/platform/convex/scim/mappers.test.ts b/services/platform/backend/core/scim/mappers.test.ts similarity index 100% rename from services/platform/convex/scim/mappers.test.ts rename to services/platform/backend/core/scim/mappers.test.ts diff --git a/services/platform/convex/scim/mappers.ts b/services/platform/backend/core/scim/mappers.ts similarity index 99% rename from services/platform/convex/scim/mappers.ts rename to services/platform/backend/core/scim/mappers.ts index 8bb538efb6..ea02d2f388 100644 --- a/services/platform/convex/scim/mappers.ts +++ b/services/platform/backend/core/scim/mappers.ts @@ -8,7 +8,7 @@ * supported (documented in the SCIM handlers). */ -import { isRecord } from '../../lib/utils/type-utils'; +import { isRecord } from '../../../lib/utils/type-utils'; import { normalizeAuthEmail } from '../lib/auth/normalize_auth_email'; import { SCIM_GROUP_SCHEMA, diff --git a/services/platform/convex/scim/responses.ts b/services/platform/backend/core/scim/responses.ts similarity index 100% rename from services/platform/convex/scim/responses.ts rename to services/platform/backend/core/scim/responses.ts diff --git a/services/platform/convex/scim/types.ts b/services/platform/backend/core/scim/types.ts similarity index 100% rename from services/platform/convex/scim/types.ts rename to services/platform/backend/core/scim/types.ts diff --git a/services/platform/convex/skills/bundle_zip.test.ts b/services/platform/backend/core/skills/bundle_zip.test.ts similarity index 98% rename from services/platform/convex/skills/bundle_zip.test.ts rename to services/platform/backend/core/skills/bundle_zip.test.ts index 68a4d3103b..edd8a6b815 100644 --- a/services/platform/convex/skills/bundle_zip.test.ts +++ b/services/platform/backend/core/skills/bundle_zip.test.ts @@ -3,11 +3,11 @@ import JSZip from 'jszip'; import { describe, expect, it } from 'vitest'; -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { MAX_SKILL_BUNDLE_FILE_BYTES, MAX_SKILL_BUNDLE_FILES, -} from '../../lib/shared/schemas/skills'; +} from '../../../lib/shared/schemas/skills'; import { parseSkillBundleZip } from './bundle_zip'; function skillMd(fields: Record, body = 'Body.\n'): string { diff --git a/services/platform/convex/skills/bundle_zip.ts b/services/platform/backend/core/skills/bundle_zip.ts similarity index 97% rename from services/platform/convex/skills/bundle_zip.ts rename to services/platform/backend/core/skills/bundle_zip.ts index 185c861924..5cb26b8e1e 100644 --- a/services/platform/convex/skills/bundle_zip.ts +++ b/services/platform/backend/core/skills/bundle_zip.ts @@ -14,15 +14,15 @@ import JSZip from 'jszip'; -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { MAX_SKILL_BUNDLE_FILE_BYTES, MAX_SKILL_BUNDLE_FILES, MAX_SKILL_BUNDLE_TOTAL_BYTES, isValidSkillSlug, type SkillFrontmatter, -} from '../../lib/shared/schemas/skills'; -import { parseSkillMd } from '../../lib/skills/parse'; +} from '../../../lib/shared/schemas/skills'; +import { parseSkillMd } from '../../../lib/skills/parse'; export interface ParsedBundleFile { /** Path relative to the bundle root (no leading slash, POSIX separators). */ diff --git a/services/platform/convex/skills/file_actions.test.ts b/services/platform/backend/core/skills/file_actions.test.ts similarity index 99% rename from services/platform/convex/skills/file_actions.test.ts rename to services/platform/backend/core/skills/file_actions.test.ts index 57a0dd4f3f..276bb49b9b 100644 --- a/services/platform/convex/skills/file_actions.test.ts +++ b/services/platform/backend/core/skills/file_actions.test.ts @@ -6,7 +6,7 @@ import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; // The `*ForViewer` functions ARE the skill-file surface now — the Convex // action wrappers that used to delegate to them retired with the runtime — diff --git a/services/platform/convex/skills/file_actions.ts b/services/platform/backend/core/skills/file_actions.ts similarity index 98% rename from services/platform/convex/skills/file_actions.ts rename to services/platform/backend/core/skills/file_actions.ts index 7e4b833a54..514390ddff 100644 --- a/services/platform/convex/skills/file_actions.ts +++ b/services/platform/backend/core/skills/file_actions.ts @@ -1,27 +1,27 @@ 'use node'; -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { isValidSkillSlug, MAX_SKILL_TEAMS, type SkillFrontmatter, -} from '../../lib/shared/schemas/skills'; +} from '../../../lib/shared/schemas/skills'; import { listOrgSkills, readOrgSkill, type OrgSkill, -} from '../../lib/skills/listing'; +} from '../../../lib/skills/listing'; import { parseSkillMd, serializeSkillMd, SkillParseError, -} from '../../lib/skills/parse'; +} from '../../../lib/skills/parse'; import { canEditSkill, canViewSkill, type SkillViewer, type UserSkillViewer, -} from '../../lib/skills/visibility'; +} from '../../../lib/skills/visibility'; import { type ParsedBundle } from './bundle_zip'; import { createOrgSkillReader, diff --git a/services/platform/convex/skills/file_utils.test.ts b/services/platform/backend/core/skills/file_utils.test.ts similarity index 99% rename from services/platform/convex/skills/file_utils.test.ts rename to services/platform/backend/core/skills/file_utils.test.ts index 61e1c97f04..a8a335b8bf 100644 --- a/services/platform/convex/skills/file_utils.test.ts +++ b/services/platform/backend/core/skills/file_utils.test.ts @@ -18,8 +18,8 @@ import { MAX_SKILL_BUNDLE_FILE_BYTES, MAX_SKILL_BUNDLE_FILES, MAX_SKILL_BUNDLE_TOTAL_BYTES, -} from '../../lib/shared/schemas/skills'; -import { readOrgSkill, readOrgSkills } from '../../lib/skills/listing'; +} from '../../../lib/shared/schemas/skills'; +import { readOrgSkill, readOrgSkills } from '../../../lib/skills/listing'; import { createOrgSkillReader, listSkillBundleFileEntries, diff --git a/services/platform/convex/skills/file_utils.ts b/services/platform/backend/core/skills/file_utils.ts similarity index 99% rename from services/platform/convex/skills/file_utils.ts rename to services/platform/backend/core/skills/file_utils.ts index 258c13b4ad..f14b9a9712 100644 --- a/services/platform/convex/skills/file_utils.ts +++ b/services/platform/backend/core/skills/file_utils.ts @@ -40,8 +40,8 @@ import { MAX_SKILL_BUNDLE_FILES, MAX_SKILL_BUNDLE_TOTAL_BYTES, MAX_SKILL_MD_BYTES, -} from '../../lib/shared/schemas/skills'; -import type { SkillBundleReader } from '../../lib/skills/listing'; +} from '../../../lib/shared/schemas/skills'; +import type { SkillBundleReader } from '../../../lib/skills/listing'; import { atomicWrite, atomicWriteBuffer, diff --git a/services/platform/convex/skills/views.ts b/services/platform/backend/core/skills/views.ts similarity index 96% rename from services/platform/convex/skills/views.ts rename to services/platform/backend/core/skills/views.ts index b8f8e6678d..65fcbbd3c6 100644 --- a/services/platform/convex/skills/views.ts +++ b/services/platform/backend/core/skills/views.ts @@ -8,7 +8,7 @@ * markdown, which is all it ever is. */ -import type { SkillVisibility } from '../../lib/shared/schemas/skills'; +import type { SkillVisibility } from '../../../lib/shared/schemas/skills'; /** The fields every skill view carries. */ export interface SkillSummaryView { diff --git a/services/platform/convex/tasks/access.test.ts b/services/platform/backend/core/tasks/access.test.ts similarity index 98% rename from services/platform/convex/tasks/access.test.ts rename to services/platform/backend/core/tasks/access.test.ts index fdf43adb32..ef2061b591 100644 --- a/services/platform/convex/tasks/access.test.ts +++ b/services/platform/backend/core/tasks/access.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { ASSIGNABLE_STATUSES, canClaimTask, diff --git a/services/platform/convex/tasks/access.ts b/services/platform/backend/core/tasks/access.ts similarity index 97% rename from services/platform/convex/tasks/access.ts rename to services/platform/backend/core/tasks/access.ts index cd039234d1..5e90d6b39b 100644 --- a/services/platform/convex/tasks/access.ts +++ b/services/platform/backend/core/tasks/access.ts @@ -15,7 +15,7 @@ export { type ProjectAccessResult, } from '../projects/access'; -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import type { TaskAssigneeType } from './types'; type TaskActorType = TaskAssigneeType; diff --git a/services/platform/convex/tasks/agent_run_host.ts b/services/platform/backend/core/tasks/agent_run_host.ts similarity index 99% rename from services/platform/convex/tasks/agent_run_host.ts rename to services/platform/backend/core/tasks/agent_run_host.ts index 855dc5ccba..719adb2b46 100644 --- a/services/platform/convex/tasks/agent_run_host.ts +++ b/services/platform/backend/core/tasks/agent_run_host.ts @@ -17,9 +17,9 @@ import { randomBytes } from 'node:crypto'; -import { buildStdinUserMessage } from '../../lib/harnesses/parsers/claude-stream-json'; -import { isHarnessSlug } from '../../lib/harnesses/types'; -import { AppError } from '../../lib/shared/errors/app-error'; +import { buildStdinUserMessage } from '../../../lib/harnesses/parsers/claude-stream-json'; +import { isHarnessSlug } from '../../../lib/harnesses/types'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { liveProgressSink, releaseTurnKey, diff --git a/services/platform/convex/tasks/audit_actions.ts b/services/platform/backend/core/tasks/audit_actions.ts similarity index 100% rename from services/platform/convex/tasks/audit_actions.ts rename to services/platform/backend/core/tasks/audit_actions.ts diff --git a/services/platform/convex/tasks/date_notification_recipients.test.ts b/services/platform/backend/core/tasks/date_notification_recipients.test.ts similarity index 100% rename from services/platform/convex/tasks/date_notification_recipients.test.ts rename to services/platform/backend/core/tasks/date_notification_recipients.test.ts diff --git a/services/platform/convex/tasks/date_notification_recipients.ts b/services/platform/backend/core/tasks/date_notification_recipients.ts similarity index 100% rename from services/platform/convex/tasks/date_notification_recipients.ts rename to services/platform/backend/core/tasks/date_notification_recipients.ts diff --git a/services/platform/convex/tasks/helpers.test.ts b/services/platform/backend/core/tasks/helpers.test.ts similarity index 100% rename from services/platform/convex/tasks/helpers.test.ts rename to services/platform/backend/core/tasks/helpers.test.ts diff --git a/services/platform/convex/tasks/helpers.ts b/services/platform/backend/core/tasks/helpers.ts similarity index 99% rename from services/platform/convex/tasks/helpers.ts rename to services/platform/backend/core/tasks/helpers.ts index 1d0d65f2cb..ad03cbe417 100644 --- a/services/platform/convex/tasks/helpers.ts +++ b/services/platform/backend/core/tasks/helpers.ts @@ -3,11 +3,11 @@ * user-facing `mutations.ts` and the agent-facing `internal_mutations.ts`. */ -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { defaultTaskLabelColor, PREDEFINED_TASK_LABELS, -} from '../../lib/shared/task-label-colors'; +} from '../../../lib/shared/task-label-colors'; import type { MutationCtx, QueryCtx } from '../lib/ctx'; import type { Doc, Id } from '../lib/rows'; import { parseIssueNumber, parseRepoRef } from './issue_ref'; diff --git a/services/platform/convex/tasks/issue_ref.test.ts b/services/platform/backend/core/tasks/issue_ref.test.ts similarity index 100% rename from services/platform/convex/tasks/issue_ref.test.ts rename to services/platform/backend/core/tasks/issue_ref.test.ts diff --git a/services/platform/convex/tasks/issue_ref.ts b/services/platform/backend/core/tasks/issue_ref.ts similarity index 100% rename from services/platform/convex/tasks/issue_ref.ts rename to services/platform/backend/core/tasks/issue_ref.ts diff --git a/services/platform/convex/tasks/mentions.test.ts b/services/platform/backend/core/tasks/mentions.test.ts similarity index 100% rename from services/platform/convex/tasks/mentions.test.ts rename to services/platform/backend/core/tasks/mentions.test.ts diff --git a/services/platform/convex/tasks/mentions.ts b/services/platform/backend/core/tasks/mentions.ts similarity index 100% rename from services/platform/convex/tasks/mentions.ts rename to services/platform/backend/core/tasks/mentions.ts diff --git a/services/platform/convex/tasks/rank.test.ts b/services/platform/backend/core/tasks/rank.test.ts similarity index 100% rename from services/platform/convex/tasks/rank.test.ts rename to services/platform/backend/core/tasks/rank.test.ts diff --git a/services/platform/convex/tasks/rank.ts b/services/platform/backend/core/tasks/rank.ts similarity index 100% rename from services/platform/convex/tasks/rank.ts rename to services/platform/backend/core/tasks/rank.ts diff --git a/services/platform/convex/tasks/review_shared.ts b/services/platform/backend/core/tasks/review_shared.ts similarity index 99% rename from services/platform/convex/tasks/review_shared.ts rename to services/platform/backend/core/tasks/review_shared.ts index 388b554484..1418a86e1c 100644 --- a/services/platform/convex/tasks/review_shared.ts +++ b/services/platform/backend/core/tasks/review_shared.ts @@ -14,8 +14,8 @@ * (mutations → internal_mutations → here; review_mutations → here). */ -import { AppError } from '../../lib/shared/errors/app-error'; -import { isRecord } from '../../lib/utils/type-utils'; +import { AppError } from '../../../lib/shared/errors/app-error'; +import { isRecord } from '../../../lib/utils/type-utils'; import { createAuditLog } from '../audit_logs/helpers'; import { dismissReviewRequestNotifications } from '../collab/dismiss_review_notifications'; import { diff --git a/services/platform/convex/tasks/task_auto_retry.ts b/services/platform/backend/core/tasks/task_auto_retry.ts similarity index 100% rename from services/platform/convex/tasks/task_auto_retry.ts rename to services/platform/backend/core/tasks/task_auto_retry.ts diff --git a/services/platform/convex/tasks/task_kick_resume.test.ts b/services/platform/backend/core/tasks/task_kick_resume.test.ts similarity index 100% rename from services/platform/convex/tasks/task_kick_resume.test.ts rename to services/platform/backend/core/tasks/task_kick_resume.test.ts diff --git a/services/platform/convex/tasks/task_kick_resume.ts b/services/platform/backend/core/tasks/task_kick_resume.ts similarity index 100% rename from services/platform/convex/tasks/task_kick_resume.ts rename to services/platform/backend/core/tasks/task_kick_resume.ts diff --git a/services/platform/convex/tasks/task_serving.test.ts b/services/platform/backend/core/tasks/task_serving.test.ts similarity index 99% rename from services/platform/convex/tasks/task_serving.test.ts rename to services/platform/backend/core/tasks/task_serving.test.ts index 0599517163..b8de76ad67 100644 --- a/services/platform/convex/tasks/task_serving.test.ts +++ b/services/platform/backend/core/tasks/task_serving.test.ts @@ -16,7 +16,7 @@ import { harnessDefinitionSchema, providerDefinitionSchema, type ProviderDefinition, -} from '../../lib/shared/schemas/providers'; +} from '../../../lib/shared/schemas/providers'; import type { ActionCtx } from '../lib/ctx'; const { diff --git a/services/platform/convex/tasks/task_serving.ts b/services/platform/backend/core/tasks/task_serving.ts similarity index 100% rename from services/platform/convex/tasks/task_serving.ts rename to services/platform/backend/core/tasks/task_serving.ts diff --git a/services/platform/convex/tasks/types.ts b/services/platform/backend/core/tasks/types.ts similarity index 100% rename from services/platform/convex/tasks/types.ts rename to services/platform/backend/core/tasks/types.ts diff --git a/services/platform/convex/trusted_headers_auth/authenticate_handler.ts b/services/platform/backend/core/trusted_headers_auth/authenticate_handler.ts similarity index 100% rename from services/platform/convex/trusted_headers_auth/authenticate_handler.ts rename to services/platform/backend/core/trusted_headers_auth/authenticate_handler.ts diff --git a/services/platform/convex/tsconfig.json b/services/platform/backend/core/tsconfig.json similarity index 100% rename from services/platform/convex/tsconfig.json rename to services/platform/backend/core/tsconfig.json diff --git a/services/platform/convex/tts/audio_mime.ts b/services/platform/backend/core/tts/audio_mime.ts similarity index 100% rename from services/platform/convex/tts/audio_mime.ts rename to services/platform/backend/core/tts/audio_mime.ts diff --git a/services/platform/convex/tts/error_codes.ts b/services/platform/backend/core/tts/error_codes.ts similarity index 98% rename from services/platform/convex/tts/error_codes.ts rename to services/platform/backend/core/tts/error_codes.ts index feea1df2af..5e8d1b81ef 100644 --- a/services/platform/convex/tts/error_codes.ts +++ b/services/platform/backend/core/tts/error_codes.ts @@ -1,5 +1,5 @@ -import { AppError } from '../../lib/shared/errors/app-error'; -import { SafeFetchError } from '../lib/http/safe_fetch'; +import { SafeFetchError } from '../../../lib/net/safe-fetch'; +import { AppError } from '../../../lib/shared/errors/app-error'; /** * Stable error tokens written to `ttsAudioChunks.error`. The client keys diff --git a/services/platform/convex/video_links/captions_parser.test.ts b/services/platform/backend/core/video_links/captions_parser.test.ts similarity index 100% rename from services/platform/convex/video_links/captions_parser.test.ts rename to services/platform/backend/core/video_links/captions_parser.test.ts diff --git a/services/platform/convex/video_links/captions_parser.ts b/services/platform/backend/core/video_links/captions_parser.ts similarity index 100% rename from services/platform/convex/video_links/captions_parser.ts rename to services/platform/backend/core/video_links/captions_parser.ts diff --git a/services/platform/convex/video_links/ingest_video_link.ts b/services/platform/backend/core/video_links/ingest_video_link.ts similarity index 99% rename from services/platform/convex/video_links/ingest_video_link.ts rename to services/platform/backend/core/video_links/ingest_video_link.ts index 28125112ce..757cfce171 100644 --- a/services/platform/convex/video_links/ingest_video_link.ts +++ b/services/platform/backend/core/video_links/ingest_video_link.ts @@ -31,7 +31,8 @@ import { promises as fs } from 'node:fs'; import { join } from 'node:path'; -import { CHAT_AUDIO_MAX_DURATION_SEC } from '../../lib/shared/file-types'; +import { sanitizeUntrustedField } from '../../../lib/chat/untrusted-content'; +import { CHAT_AUDIO_MAX_DURATION_SEC } from '../../../lib/shared/file-types'; import { joinSegmentsWithParagraphs, CAPTION_PROFILE, @@ -44,7 +45,6 @@ import { orgSlugFromIdOrNull } from '../lib/helpers/org_slug'; import type { Id } from '../lib/rows'; import { deleteBlob, putBlob } from '../lib/storage/blob_access'; import { convexStorageId, type BlobRef } from '../lib/storage/blob_ref'; -import { sanitizeUntrustedField } from '../lib/untrusted_content'; import { captionsToParagraphSegments, parseVtt, diff --git a/services/platform/convex/video_links/internal_mutations.ts b/services/platform/backend/core/video_links/internal_mutations.ts similarity index 100% rename from services/platform/convex/video_links/internal_mutations.ts rename to services/platform/backend/core/video_links/internal_mutations.ts diff --git a/services/platform/convex/video_links/url_safety.test.ts b/services/platform/backend/core/video_links/url_safety.test.ts similarity index 100% rename from services/platform/convex/video_links/url_safety.test.ts rename to services/platform/backend/core/video_links/url_safety.test.ts diff --git a/services/platform/convex/video_links/url_safety.ts b/services/platform/backend/core/video_links/url_safety.ts similarity index 97% rename from services/platform/convex/video_links/url_safety.ts rename to services/platform/backend/core/video_links/url_safety.ts index d7fd372ecc..51f7e3a437 100644 --- a/services/platform/convex/video_links/url_safety.ts +++ b/services/platform/backend/core/video_links/url_safety.ts @@ -2,8 +2,8 @@ import { promises as dns } from 'node:dns'; -import { isPlaylistUrl, isSafeVideoUrl } from '../../lib/shared/video-url'; -import { isPrivateIp } from '../lib/http/safe_fetch'; +import { isPrivateIp } from '../../../lib/net/safe-fetch'; +import { isPlaylistUrl, isSafeVideoUrl } from '../../../lib/shared/video-url'; /** * Server-side SSRF guard for yt-dlp invocations. diff --git a/services/platform/convex/video_links/ytdlp.test.ts b/services/platform/backend/core/video_links/ytdlp.test.ts similarity index 100% rename from services/platform/convex/video_links/ytdlp.test.ts rename to services/platform/backend/core/video_links/ytdlp.test.ts diff --git a/services/platform/convex/video_links/ytdlp.ts b/services/platform/backend/core/video_links/ytdlp.ts similarity index 100% rename from services/platform/convex/video_links/ytdlp.ts rename to services/platform/backend/core/video_links/ytdlp.ts diff --git a/services/platform/convex/video_links/ytdlp_toolchain.ts b/services/platform/backend/core/video_links/ytdlp_toolchain.ts similarity index 100% rename from services/platform/convex/video_links/ytdlp_toolchain.ts rename to services/platform/backend/core/video_links/ytdlp_toolchain.ts diff --git a/services/platform/backend/core/webdav/README.md b/services/platform/backend/core/webdav/README.md new file mode 100644 index 0000000000..66f43b0b74 --- /dev/null +++ b/services/platform/backend/core/webdav/README.md @@ -0,0 +1,26 @@ +# WebDAV handlers — trust boundary + +This directory holds the storage-side handlers behind the WebDAV door. They +split into two visibility classes: + +## Public (UI-callable, full session check) + +- `app_password_mutations.createAppPassword` — generates a random secret + HMAC hash, inserts a row, returns plaintext **once** +- `app_password_mutations.revokeAppPassword` — soft-revoke a row owned by the caller +- `app_password_queries.listAppPasswords` — list rows owned by the caller (metadata only — no hash, no plaintext) + +These run behind the session-authenticated settings routes and only operate on rows where `userId` is the caller's. + +## Internal (called by the WebDAV protocol layer) + +Everything else (`*_internal`, `findCandidatesByPrefix`, `recordAppPasswordUse`, all `lock_*` and `tree_*` functions). + +The WebDAV protocol layer (`lib/webdav/`, mounted at `/dav` by `backend/domains/webdav/routes.ts`) holds the trust: it parses the `Authorization: Basic` header, verifies the app-password via `findCandidatesByPrefix` + HMAC comparison, then asserts `organizationId` + `userId` into every name-addressed handler call. The handlers trust those assertions — they do **not** re-check user identity. + +**Why this split**: WebDAV uses HTTP Basic — the credentials are not a session cookie the normal auth middleware can validate, so exactly one layer (the protocol dispatcher) owns credential verification, and everything behind it takes identity as input. + +**Hub-only visibility**: WebDAV applies no team ACLs (the trust split above asserts only `organizationId` + `userId`), but the tree functions do enforce document scope: project-scoped documents (`documents.projectId` set) are **not** WebDAV resources (#2545). `webdav/visibility.ts` (`isWebdavVisibleDocument`, built on `documents/access.ts`'s `isProjectScopedDocument`) gates every listing, leaf resolution, and name-collision lookup — project files never list, resolve as not-found for every caller (mirroring the REST 404s), and a PUT whose name collides with one creates an independent hub document. Project members reach those files through the project surfaces instead. + +Project-scoped **folders** (`folders.projectId` set) are excluded the same way, but at the index rather than a predicate: every folder listing, path segment, and name-collision lookup in `tree_queries.ts` / `tree_mutations.ts` (and the shared `folders/find_folder_by_path.ts`) queries `by_org_project_parent_name` pinned to `projectId=undefined`, so a project folder never lists, never resolves, and never counts as a MKCOL/PUT collision. Recursive descendant walks (cascade delete, copy, move fixup, hold guard) stay on `by_org_parent_name` — they descend from an already-hub-authorized root, and a folder's children share its scope by invariant. + +**HMAC secret**: `WEBDAV_APP_PASSWORD_HMAC_KEY` (hex-encoded 32-byte random). Derived deterministically from `INSTANCE_SECRET` by `docker-entrypoint.sh` (prod) and `server.ts` (dev) — operators do not set this manually. To rotate the HMAC independently of `INSTANCE_SECRET`, set `WEBDAV_APP_PASSWORD_HMAC_KEY` explicitly in `.env`; an explicit value always overrides the derived one. **Rotating it invalidates every existing app-password.** diff --git a/services/platform/convex/webdav/SMOKE.md b/services/platform/backend/core/webdav/SMOKE.md similarity index 100% rename from services/platform/convex/webdav/SMOKE.md rename to services/platform/backend/core/webdav/SMOKE.md diff --git a/services/platform/convex/webdav/helpers.ts b/services/platform/backend/core/webdav/helpers.ts similarity index 100% rename from services/platform/convex/webdav/helpers.ts rename to services/platform/backend/core/webdav/helpers.ts diff --git a/services/platform/convex/websites/create_website.ts b/services/platform/backend/core/websites/create_website.ts similarity index 96% rename from services/platform/convex/websites/create_website.ts rename to services/platform/backend/core/websites/create_website.ts index 15765dd33b..8e0a651973 100644 --- a/services/platform/convex/websites/create_website.ts +++ b/services/platform/backend/core/websites/create_website.ts @@ -3,7 +3,7 @@ * Does NOT register with the crawler — that's handled by the calling action. */ -import { AppError } from '../../lib/shared/errors/app-error'; +import { AppError } from '../../../lib/shared/errors/app-error'; import type { MutationCtx } from '../lib/ctx'; import type { Id } from '../lib/rows'; diff --git a/services/platform/convex/websites/internal_actions.ts b/services/platform/backend/core/websites/internal_actions.ts similarity index 100% rename from services/platform/convex/websites/internal_actions.ts rename to services/platform/backend/core/websites/internal_actions.ts diff --git a/services/platform/convex/websites/match_website_search.ts b/services/platform/backend/core/websites/match_website_search.ts similarity index 100% rename from services/platform/convex/websites/match_website_search.ts rename to services/platform/backend/core/websites/match_website_search.ts diff --git a/services/platform/convex/websites/scan_scheduling.test.ts b/services/platform/backend/core/websites/scan_scheduling.test.ts similarity index 100% rename from services/platform/convex/websites/scan_scheduling.test.ts rename to services/platform/backend/core/websites/scan_scheduling.test.ts diff --git a/services/platform/convex/websites/scan_scheduling.ts b/services/platform/backend/core/websites/scan_scheduling.ts similarity index 100% rename from services/platform/convex/websites/scan_scheduling.ts rename to services/platform/backend/core/websites/scan_scheduling.ts diff --git a/services/platform/convex/websites/types.ts b/services/platform/backend/core/websites/types.ts similarity index 100% rename from services/platform/convex/websites/types.ts rename to services/platform/backend/core/websites/types.ts diff --git a/services/platform/backend/domains/agent_secrets/service.ts b/services/platform/backend/domains/agent_secrets/service.ts index 93aaf6cc61..65af9f8d59 100644 --- a/services/platform/backend/domains/agent_secrets/service.ts +++ b/services/platform/backend/domains/agent_secrets/service.ts @@ -6,12 +6,12 @@ import { MAX_AGENT_SECRETS_PER_ORG, validateAgentSecretName, validateAgentSecretValue, -} from '../../../convex/agent_secrets/constants.ts'; +} from '../../core/agent_secrets/constants.ts'; import { decryptSecret, encryptSecret, type EncryptedSecret, -} from '../../../convex/lib/secret_box.ts'; +} from '../../core/lib/secret_box.ts'; import { toJson } from '../../db/sql.ts'; import { emitHintInTx } from '../../realtime/outbox.ts'; import { createAuditLog } from '../audit_logs/service.ts'; diff --git a/services/platform/backend/domains/agents/routes.ts b/services/platform/backend/domains/agents/routes.ts index ef6d1feb38..c8474e6afa 100644 --- a/services/platform/backend/domains/agents/routes.ts +++ b/services/platform/backend/domains/agents/routes.ts @@ -2,6 +2,11 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; +import { defineAbilityFor } from '../../../lib/permissions/ability.ts'; +import { AppError } from '../../../lib/shared/errors/app-error'; +import type { Auth } from '../../auth/auth.ts'; +import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; +import { requireSession } from '../../auth/session.ts'; import { deleteAgentForCaller, listAgentsForCaller, @@ -11,12 +16,7 @@ import { restoreFromHistoryForCaller, saveAgentForCaller, type AgentCallerArgs, -} from '../../../convex/agents/file_actions.ts'; -import { defineAbilityFor } from '../../../lib/permissions/ability.ts'; -import { AppError } from '../../../lib/shared/errors/app-error'; -import type { Auth } from '../../auth/auth.ts'; -import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; -import { requireSession } from '../../auth/session.ts'; +} from '../../core/agents/file_actions.ts'; import { resolveOrgSlug } from '../../lib/org-config.ts'; /** diff --git a/services/platform/backend/domains/approvals/gate.ts b/services/platform/backend/domains/approvals/gate.ts index c78d58a025..13a9528659 100644 --- a/services/platform/backend/domains/approvals/gate.ts +++ b/services/platform/backend/domains/approvals/gate.ts @@ -1,6 +1,6 @@ import type { Sql } from 'postgres'; -import { resolveApprovalRequirement } from '../../../convex/approvals/policy.ts'; +import { resolveApprovalRequirement } from '../../core/approvals/policy.ts'; import { toJson } from '../../db/sql.ts'; import { readGovernancePolicyForOrg } from '../../lib/org-config.ts'; diff --git a/services/platform/backend/domains/audit_logs/routes.ts b/services/platform/backend/domains/audit_logs/routes.ts index ceeff3b24f..702905cfd3 100644 --- a/services/platform/backend/domains/audit_logs/routes.ts +++ b/services/platform/backend/domains/audit_logs/routes.ts @@ -2,16 +2,16 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; -import { - browserFacing, - s3PresignGetUrl, - s3PutObject, -} from '../../../convex/lib/storage/object_store.ts'; import { authorizeRls } from '../../auth/access.ts'; import type { Auth } from '../../auth/auth.ts'; import { isAdminRole } from '../../auth/membership.ts'; import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; import { requireSession } from '../../auth/session.ts'; +import { + browserFacing, + s3PresignGetUrl, + s3PutObject, +} from '../../core/lib/storage/object_store.ts'; import { resolveObjectStore } from '../../lib/object-store.ts'; import { resolveOrgSlug } from '../../lib/org-config.ts'; import { listBlockCounters } from '../login_attempts/service.ts'; diff --git a/services/platform/backend/domains/audit_logs/service.ts b/services/platform/backend/domains/audit_logs/service.ts index a3e1cc7a70..c3e4952eff 100644 --- a/services/platform/backend/domains/audit_logs/service.ts +++ b/services/platform/backend/domains/audit_logs/service.ts @@ -1,6 +1,6 @@ import type { Sql, TransactionSql } from 'postgres'; -import { computeAuditHash } from '../../../convex/lib/helpers/audit_hash.ts'; +import { computeAuditHash } from '../../core/lib/helpers/audit_hash.ts'; import { toJson } from '../../db/sql.ts'; import { buildAuditRecordHashInput, diff --git a/services/platform/backend/domains/audit_logs/verify.ts b/services/platform/backend/domains/audit_logs/verify.ts index 48935ac231..636e7022a3 100644 --- a/services/platform/backend/domains/audit_logs/verify.ts +++ b/services/platform/backend/domains/audit_logs/verify.ts @@ -1,6 +1,6 @@ import type { Sql } from 'postgres'; -import { computeAuditHash } from '../../../convex/lib/helpers/audit_hash.ts'; +import { computeAuditHash } from '../../core/lib/helpers/audit_hash.ts'; import { writeNotificationForOrgs } from '../notifications/service.ts'; import { rowToHashInput } from './hash-input.ts'; import type { AuditLogRow } from './types.ts'; diff --git a/services/platform/backend/domains/automations/dispatch-store.ts b/services/platform/backend/domains/automations/dispatch-store.ts index 52712b48db..da69c33c58 100644 --- a/services/platform/backend/domains/automations/dispatch-store.ts +++ b/services/platform/backend/domains/automations/dispatch-store.ts @@ -1,9 +1,5 @@ import type { Sql } from 'postgres'; -import { - boundRunTrace, - truncateRunDetail, -} from '../../../convex/automations/bound_run_payload.ts'; import type { DispatchStore, RunDetail, @@ -13,6 +9,10 @@ import type { } from '../../../lib/engine/api/dispatch.ts'; import type { Automation } from '../../../lib/engine/core/types.ts'; import { defineAbilityFor } from '../../../lib/permissions/ability.ts'; +import { + boundRunTrace, + truncateRunDetail, +} from '../../core/automations/bound_run_payload.ts'; import { toJson } from '../../db/sql.ts'; import { createAuditLog } from '../audit_logs/service.ts'; import { diff --git a/services/platform/backend/domains/automations/reattach.ts b/services/platform/backend/domains/automations/reattach.ts index 4ff4b9243a..cd500fa8d6 100644 --- a/services/platform/backend/domains/automations/reattach.ts +++ b/services/platform/backend/domains/automations/reattach.ts @@ -1,7 +1,7 @@ import type { Sql } from 'postgres'; -import { sessionExecStatus } from '../../../convex/node_only/sandbox/helpers/session_client.ts'; -import { sessionOpLastSignOfLifeMs } from '../../../convex/sandbox/agent_deadline.ts'; +import { sessionExecStatus } from '../../core/node_only/sandbox/helpers/session_client.ts'; +import { sessionOpLastSignOfLifeMs } from '../../core/sandbox/agent_deadline.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import { claimRecoveryResume, RECOVERY_STALE_MS } from '../sandbox/recovery.ts'; diff --git a/services/platform/backend/domains/automations/routes.ts b/services/platform/backend/domains/automations/routes.ts index d7d36afb2f..6aa0b36afd 100644 --- a/services/platform/backend/domains/automations/routes.ts +++ b/services/platform/backend/domains/automations/routes.ts @@ -2,9 +2,6 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; -import { runSessionWithStore } from '../../../convex/automations_builder/run_session.ts'; -import { loadConnectorDefinitions } from '../../../convex/connector_credentials/connector_catalog.ts'; -import { resolveWorkflowAgentServing } from '../../../convex/lib/providers/agent_serving.ts'; import { registerConnector } from '../../../lib/connectors/registry.ts'; import { nodeTypes } from '../../../lib/engine/core/slots.ts'; import { AppError } from '../../../lib/shared/errors/app-error'; @@ -12,7 +9,10 @@ import type { Auth } from '../../auth/auth.ts'; import { isAdminOrDeveloperRole } from '../../auth/membership.ts'; import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; import { requireSession } from '../../auth/session.ts'; -import { createCtxShim } from '../../lib/convex-shim.ts'; +import { runSessionWithStore } from '../../core/automations_builder/run_session.ts'; +import { loadConnectorDefinitions } from '../../core/connector_credentials/connector_catalog.ts'; +import { resolveWorkflowAgentServing } from '../../core/lib/providers/agent_serving.ts'; +import { createCtxShim } from '../../lib/ctx-shim.ts'; import { resolveOrgSlug } from '../../lib/org-config.ts'; import { knowledgeShimHandlers } from '../knowledge/service.ts'; import { pgAutomationStore } from './dispatch-store.ts'; diff --git a/services/platform/backend/domains/automations/shim.ts b/services/platform/backend/domains/automations/shim.ts index c861a48290..ef4617db06 100644 --- a/services/platform/backend/domains/automations/shim.ts +++ b/services/platform/backend/domains/automations/shim.ts @@ -4,7 +4,7 @@ import { ConnectorError } from '../../../lib/connectors/errors.ts'; import { AppError } from '../../../lib/shared/errors/app-error'; import { toJson } from '../../db/sql.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; -import type { ShimHandlers, ShimScheduler } from '../../lib/convex-shim.ts'; +import type { ShimHandlers, ShimScheduler } from '../../lib/ctx-shim.ts'; import { evaluateApprovalGate } from '../approvals/gate.ts'; import { dismissAgentQuestionNotifications, diff --git a/services/platform/backend/domains/automations/store.ts b/services/platform/backend/domains/automations/store.ts index 393e5129f8..50c2599ba0 100644 --- a/services/platform/backend/domains/automations/store.ts +++ b/services/platform/backend/domains/automations/store.ts @@ -3,7 +3,7 @@ import type { Sql, TransactionSql } from 'postgres'; import { hashWebhookToken, mintWebhookToken, -} from '../../../convex/automations/webhook_token.ts'; +} from '../../core/automations/webhook_token.ts'; import { toJson } from '../../db/sql.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import { emitHintInTx } from '../../realtime/outbox.ts'; diff --git a/services/platform/backend/domains/automations/triggers.ts b/services/platform/backend/domains/automations/triggers.ts index de46f39e21..04df205bce 100644 --- a/services/platform/backend/domains/automations/triggers.ts +++ b/services/platform/backend/domains/automations/triggers.ts @@ -1,12 +1,12 @@ import { Hono } from 'hono'; import type { Sql, TransactionSql } from 'postgres'; -import { dueOccurrence } from '../../../convex/automations/cron.ts'; +import { dueOccurrence } from '../../core/automations/cron.ts'; import { hashWebhookToken, isPlausibleWebhookToken, tokenHashEquals, -} from '../../../convex/automations/webhook_token.ts'; +} from '../../core/automations/webhook_token.ts'; import { AutomationError, beginRun, beginRunInTx } from './store.ts'; /** diff --git a/services/platform/backend/domains/automations/upload.ts b/services/platform/backend/domains/automations/upload.ts index 0367a1789a..1467bcdaea 100644 --- a/services/platform/backend/domains/automations/upload.ts +++ b/services/platform/backend/domains/automations/upload.ts @@ -1,19 +1,19 @@ import type { Sql } from 'postgres'; +import { defineAbilityFor } from '../../../lib/permissions/ability.ts'; import { uploadAutomationImpl, type UploadArgs, type UploadResult, -} from '../../../convex/automations/upload_impl.ts'; +} from '../../core/automations/upload_impl.ts'; import { parseBlobRef, s3KeyBelongsToOrg, -} from '../../../convex/lib/storage/blob_ref.ts'; +} from '../../core/lib/storage/blob_ref.ts'; import { s3DeleteObject, s3GetObjectBytes, -} from '../../../convex/lib/storage/object_store.ts'; -import { defineAbilityFor } from '../../../lib/permissions/ability.ts'; +} from '../../core/lib/storage/object_store.ts'; import { resolveObjectStore } from '../../lib/object-store.ts'; import { bindProject, saveVersion } from './store.ts'; diff --git a/services/platform/backend/domains/branding/service.ts b/services/platform/backend/domains/branding/service.ts index a0c912fd5f..19f14cc290 100644 --- a/services/platform/backend/domains/branding/service.ts +++ b/services/platform/backend/domains/branding/service.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import type { Sql } from 'postgres'; +import { brandingJsonSchema } from '../../../lib/shared/schemas/branding.ts'; import { buildBrandingImageUrl, MAX_FILE_SIZE_BYTES, @@ -18,7 +19,7 @@ import { validateImageType, type BrandingJsonConfig, type BrandingReadResult, -} from '../../../convex/branding/file_utils.ts'; +} from '../../core/branding/file_utils.ts'; import { atomicWrite, atomicWriteBuffer, @@ -27,8 +28,7 @@ import { pruneHistory, readFileSafe, readJsonFile, -} from '../../../convex/lib/file_io.ts'; -import { brandingJsonSchema } from '../../../lib/shared/schemas/branding.ts'; +} from '../../core/lib/file_io.ts'; /** * Branding file I/O — the 0.5 twin of `convex/branding/file_actions.ts` diff --git a/services/platform/backend/domains/browser_sessions/service.ts b/services/platform/backend/domains/browser_sessions/service.ts index 9a0bfb73ea..5106d15d2a 100644 --- a/services/platform/backend/domains/browser_sessions/service.ts +++ b/services/platform/backend/domains/browser_sessions/service.ts @@ -1,7 +1,7 @@ import type { Sql, TransactionSql } from 'postgres'; -import { decideInstanceAdmin } from '../../../convex/deployment/auth_policy.ts'; -import { encryptString } from '../../../convex/lib/crypto/encrypt_string.ts'; +import { decideInstanceAdmin } from '../../core/deployment/auth_policy.ts'; +import { encryptString } from '../../core/lib/crypto/encrypt_string.ts'; /** * Browser-session pool — the 0.5 twin of `convex/browser_sessions`: warmed diff --git a/services/platform/backend/domains/changelog/service.ts b/services/platform/backend/domains/changelog/service.ts index 43e387a35b..961ef45398 100644 --- a/services/platform/backend/domains/changelog/service.ts +++ b/services/platform/backend/domains/changelog/service.ts @@ -1,5 +1,5 @@ -import { fetchReleasesPageImpl } from '../../../convex/changelog/internal_actions.ts'; import { compareVersions } from '../../../lib/compare-versions.ts'; +import { fetchReleasesPageImpl } from '../../core/changelog/internal_actions.ts'; /** * The in-app changelog — the 0.5 twin of `convex/changelog/*`: the GitHub diff --git a/services/platform/backend/domains/chat/composer.ts b/services/platform/backend/domains/chat/composer.ts index 807c044644..01f97119da 100644 --- a/services/platform/backend/domains/chat/composer.ts +++ b/services/platform/backend/domains/chat/composer.ts @@ -1,14 +1,14 @@ import type { Sql } from 'postgres'; -import { collectComposerOptions } from '../../../convex/chat/composer.ts'; -import { listConnectorSummaries } from '../../../convex/connector_credentials/connector_catalog.ts'; -import { walkChatCatalog } from '../../../convex/lib/providers/chat_catalog.ts'; +import { collectComposerOptions } from '../../core/chat/composer.ts'; +import { listConnectorSummaries } from '../../core/connector_credentials/connector_catalog.ts'; +import { walkChatCatalog } from '../../core/lib/providers/chat_catalog.ts'; import { loadHarnesses, readSystemEntryIcon, -} from '../../../convex/lib/providers/load_system_config.ts'; -import { listSkillsForViewer } from '../../../convex/skills/file_actions.ts'; -import { createCtxShim } from '../../lib/convex-shim.ts'; +} from '../../core/lib/providers/load_system_config.ts'; +import { listSkillsForViewer } from '../../core/skills/file_actions.ts'; +import { createCtxShim } from '../../lib/ctx-shim.ts'; import { resolveOrgSlug } from '../../lib/org-config.ts'; import { listConnectedConnectorSlugs } from '../connector_credentials/service.ts'; import { getAccessibleModelsForUser } from '../governance/service.ts'; diff --git a/services/platform/backend/domains/chat/routes.ts b/services/platform/backend/domains/chat/routes.ts index 863d730bcd..23c59a5f46 100644 --- a/services/platform/backend/domains/chat/routes.ts +++ b/services/platform/backend/domains/chat/routes.ts @@ -3,7 +3,6 @@ import { streamSSE } from 'hono/streaming'; import type { Sql } from 'postgres'; import { z } from 'zod'; -import { sanitizeError } from '../../../convex/lib/utils/sanitize_secrets.ts'; import { classifyChatErrorCode, encodeChatError, @@ -12,6 +11,7 @@ import type { Auth } from '../../auth/auth.ts'; import { isAdminOrDeveloperRole } from '../../auth/membership.ts'; import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; import { requireSession } from '../../auth/session.ts'; +import { sanitizeError } from '../../core/lib/utils/sanitize_secrets.ts'; import { emitHintInTx } from '../../realtime/outbox.ts'; import { isBackendDraining } from '../control/service.ts'; import { LegalHoldError } from '../legal_holds/service.ts'; diff --git a/services/platform/backend/domains/chat/service.ts b/services/platform/backend/domains/chat/service.ts index ad175047ea..20181aa14f 100644 --- a/services/platform/backend/domains/chat/service.ts +++ b/services/platform/backend/domains/chat/service.ts @@ -1,11 +1,11 @@ import type { Sql } from 'postgres'; +import type { TurnOutcome } from '../../../lib/chat/turn.ts'; import { executeTurn, type ExecuteTurnArgs, -} from '../../../convex/chat/turn_action.ts'; -import type { TurnOutcome } from '../../../lib/chat/turn.ts'; -import { createCtxShim } from '../../lib/convex-shim.ts'; +} from '../../core/chat/turn_action.ts'; +import { createCtxShim } from '../../lib/ctx-shim.ts'; import { chatShimHandlers } from './shim.ts'; import { createPgTurnStore, createPgUsageLedger } from './store.ts'; diff --git a/services/platform/backend/domains/chat/shim.ts b/services/platform/backend/domains/chat/shim.ts index 7fd391e5d9..a8f08cb348 100644 --- a/services/platform/backend/domains/chat/shim.ts +++ b/services/platform/backend/domains/chat/shim.ts @@ -1,7 +1,7 @@ import type { Sql } from 'postgres'; import { findOrganizationMember } from '../../auth/membership.ts'; -import type { ShimHandlers } from '../../lib/convex-shim.ts'; +import type { ShimHandlers } from '../../lib/ctx-shim.ts'; import { createAuditLog } from '../audit_logs/service.ts'; import { searchConversationsForChat } from '../conversations/search-chat.ts'; import { listDocumentsForAgent } from '../documents/agent-list.ts'; @@ -37,7 +37,7 @@ import { getThreadLineageIds, setThreadTitleIfAbsent } from './threads.ts'; * database, and the tool layer already words empties for the model. * * Everything stays fail-loud for names NOT in this map — a new ctx call in - * 0.4 surfaces as `[convex-shim] un-shimmed …` naming the function. + * 0.4 surfaces as `[ctx-shim] un-shimmed …` naming the function. */ // 0.4 pagination contract the entity legs expect. The 0.5 cursor is a plain diff --git a/services/platform/backend/domains/chat/threads.ts b/services/platform/backend/domains/chat/threads.ts index cfe0758b2a..43121cf153 100644 --- a/services/platform/backend/domains/chat/threads.ts +++ b/services/platform/backend/domains/chat/threads.ts @@ -2,11 +2,11 @@ import { randomBytes } from 'node:crypto'; import type { Sql, TransactionSql } from 'postgres'; -import { checkProjectAccess } from '../../../convex/projects/access.ts'; import { getUserTeamIds, findOrganizationMember, } from '../../auth/membership.ts'; +import { checkProjectAccess } from '../../core/projects/access.ts'; import { toJson } from '../../db/sql.ts'; import { createAuditLog } from '../audit_logs/service.ts'; import { assertNotHeld, loadActiveHolds } from '../legal_holds/service.ts'; diff --git a/services/platform/backend/domains/cloud_import/routes.ts b/services/platform/backend/domains/cloud_import/routes.ts index 43a8fee5b4..59cf4ba3ef 100644 --- a/services/platform/backend/domains/cloud_import/routes.ts +++ b/services/platform/backend/domains/cloud_import/routes.ts @@ -2,6 +2,12 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; +import { defineAbilityFor } from '../../../lib/permissions/ability.ts'; +import { getString, isRecord } from '../../../lib/utils/type-utils.ts'; +import type { Auth } from '../../auth/auth.ts'; +import { findOrganizationMember } from '../../auth/membership.ts'; +import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; +import { requireSession, type SessionBundle } from '../../auth/session.ts'; import { cloudImportMicrosoftTenantMissingEnvNames, cloudImportOauthMissingEnvNames, @@ -9,33 +15,27 @@ import { resolveCloudImportOauthRedirectUri, resolveDocumentsUrl, resolveMicrosoftCloudImportTenantId, -} from '../../../convex/cloud_import/deployment_config.ts'; +} from '../../core/cloud_import/deployment_config.ts'; import { getCloudImportProviderEndpoints, type CloudImportProviderEndpoints, -} from '../../../convex/cloud_import/providers.ts'; +} from '../../core/cloud_import/providers.ts'; import { EntraIssuerError, extractTenantId, -} from '../../../convex/enterprise_sso/entra_id/constants.ts'; -import { generatePkcePair } from '../../../convex/enterprise_sso/pkce.ts'; -import { buildAuthorizeUrl } from '../../../convex/http_connectors/authorize_url.ts'; +} from '../../core/enterprise_sso/entra_id/constants.ts'; +import { generatePkcePair } from '../../core/enterprise_sso/pkce.ts'; +import { buildAuthorizeUrl } from '../../core/http_connectors/authorize_url.ts'; import { renderConnectorErrorPage, type ConnectorErrorKind, -} from '../../../convex/http_connectors/error_page.ts'; +} from '../../core/http_connectors/error_page.ts'; import { hashStateToken, isPlausibleStateToken, mintStateToken, -} from '../../../convex/http_connectors/oauth_state.ts'; -import { exchangeAuthorizationCode } from '../../../convex/http_connectors/token_exchange.ts'; -import { defineAbilityFor } from '../../../lib/permissions/ability.ts'; -import { getString, isRecord } from '../../../lib/utils/type-utils.ts'; -import type { Auth } from '../../auth/auth.ts'; -import { findOrganizationMember } from '../../auth/membership.ts'; -import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; -import { requireSession, type SessionBundle } from '../../auth/session.ts'; +} from '../../core/http_connectors/oauth_state.ts'; +import { exchangeAuthorizationCode } from '../../core/http_connectors/token_exchange.ts'; import { getCloudImportAppStatus, resolveCloudImportApp, diff --git a/services/platform/backend/domains/cloud_import/service.ts b/services/platform/backend/domains/cloud_import/service.ts index 8cff4f4152..d4a19e40ef 100644 --- a/services/platform/backend/domains/cloud_import/service.ts +++ b/services/platform/backend/domains/cloud_import/service.ts @@ -1,21 +1,21 @@ import type { Sql, TransactionSql } from 'postgres'; -import { resolveMicrosoftCloudImportTenantId } from '../../../convex/cloud_import/deployment_config.ts'; +import { resolveMicrosoftCloudImportTenantId } from '../../core/cloud_import/deployment_config.ts'; import { refreshGoogleAccessToken, refreshMicrosoftAccessToken, type RefreshedTokens, -} from '../../../convex/cloud_import/token_refresh.ts'; +} from '../../core/cloud_import/token_refresh.ts'; import { parseSecretPayload, type ConnectorSecretPayload, -} from '../../../convex/connector_credentials/auth_injection.ts'; -import { OAUTH_STATE_TTL_MS } from '../../../convex/http_connectors/oauth_state.ts'; +} from '../../core/connector_credentials/auth_injection.ts'; +import { OAUTH_STATE_TTL_MS } from '../../core/http_connectors/oauth_state.ts'; import { decryptSecret, encryptSecret, type EncryptedSecret, -} from '../../../convex/lib/secret_box.ts'; +} from '../../core/lib/secret_box.ts'; import { toJson } from '../../db/sql.ts'; import { resolveCloudImportApp } from '../connectors/oauth-apps.ts'; diff --git a/services/platform/backend/domains/collab/email-sink.ts b/services/platform/backend/domains/collab/email-sink.ts index 5864ad37b2..ea076c81ab 100644 --- a/services/platform/backend/domains/collab/email-sink.ts +++ b/services/platform/backend/domains/collab/email-sink.ts @@ -1,15 +1,15 @@ import type { Sql } from 'postgres'; -import { sendConnectorAction } from '../../../convex/conversations/connector_slug.ts'; -import { ACTIONABLE_EMAIL_CONNECTORS } from '../../../convex/notifications/actionable_email_connectors.ts'; +import { defaultLocale as appDefaultLocale } from '../../../lib/i18n/config.ts'; +import { clampToSupportedLocale } from '../../../lib/shared/utils/get-organization-default-locale.ts'; +import { sendConnectorAction } from '../../core/conversations/connector_slug.ts'; +import { ACTIONABLE_EMAIL_CONNECTORS } from '../../core/notifications/actionable_email_connectors.ts'; import { buildActionableEmailInput, pickSendableMailbox, -} from '../../../convex/notifications/actionable_email_input.ts'; -import { renderActionableEmailContent } from '../../../convex/notifications/notification_messages.ts'; -import { buildPersonalNotificationUrl } from '../../../convex/notifications/personal_notification_url.ts'; -import { defaultLocale as appDefaultLocale } from '../../../lib/i18n/config.ts'; -import { clampToSupportedLocale } from '../../../lib/shared/utils/get-organization-default-locale.ts'; +} from '../../core/notifications/actionable_email_input.ts'; +import { renderActionableEmailContent } from '../../core/notifications/notification_messages.ts'; +import { buildPersonalNotificationUrl } from '../../core/notifications/personal_notification_url.ts'; import type { TaskPayloads } from '../../jobs/tasks.ts'; import { runConnectorAction } from '../connectors/service.ts'; diff --git a/services/platform/backend/domains/collab/mention-directory.ts b/services/platform/backend/domains/collab/mention-directory.ts index 54ccee411f..b2ba4606e2 100644 --- a/services/platform/backend/domains/collab/mention-directory.ts +++ b/services/platform/backend/domains/collab/mention-directory.ts @@ -1,12 +1,12 @@ import type { Sql, TransactionSql } from 'postgres'; -import { hasProjectAccess } from '../../../convex/projects/access.ts'; +import { hasProjectAccess } from '../../core/projects/access.ts'; import { extractMentions, findUnresolvedMentionTokens, type MentionDirectoryEntry, type ResolvedMention, -} from '../../../convex/tasks/mentions.ts'; +} from '../../core/tasks/mentions.ts'; import { listAutomations } from '../automations/store.ts'; /** diff --git a/services/platform/backend/domains/collab/service.ts b/services/platform/backend/domains/collab/service.ts index c95af066f6..0f0df83df6 100644 --- a/services/platform/backend/domains/collab/service.ts +++ b/services/platform/backend/domains/collab/service.ts @@ -1,10 +1,10 @@ import type { Sql, TransactionSql } from 'postgres'; +import { isActionableNotificationType } from '../../../lib/shared/attention.ts'; import { coalesceKeyFor, NOTIFICATION_EMAIL_DEBOUNCE_MS, -} from '../../../convex/collab/coalesce.ts'; -import { isActionableNotificationType } from '../../../lib/shared/attention.ts'; +} from '../../core/collab/coalesce.ts'; import { toJson } from '../../db/sql.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import { emitHintInTx } from '../../realtime/outbox.ts'; diff --git a/services/platform/backend/domains/connector_credentials/routes.ts b/services/platform/backend/domains/connector_credentials/routes.ts index 5b93f050f2..fd11a1a820 100644 --- a/services/platform/backend/domains/connector_credentials/routes.ts +++ b/services/platform/backend/domains/connector_credentials/routes.ts @@ -2,12 +2,12 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; -import { listConnectorSummaries } from '../../../convex/connector_credentials/connector_catalog.ts'; -import { resolveOauthAppCredentials } from '../../../convex/http_connectors/deployment_config.ts'; import { defineAbilityFor } from '../../../lib/permissions/ability.ts'; import type { Auth } from '../../auth/auth.ts'; import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; import { requireSession } from '../../auth/session.ts'; +import { listConnectorSummaries } from '../../core/connector_credentials/connector_catalog.ts'; +import { resolveOauthAppCredentials } from '../../core/http_connectors/deployment_config.ts'; import { listOauthApps } from '../connectors/oauth-apps.ts'; import { ConnectorCredentialError, diff --git a/services/platform/backend/domains/connector_credentials/service.ts b/services/platform/backend/domains/connector_credentials/service.ts index 8a14693be4..c0dec2c07a 100644 --- a/services/platform/backend/domains/connector_credentials/service.ts +++ b/services/platform/backend/domains/connector_credentials/service.ts @@ -1,26 +1,26 @@ import type { Sql, TransactionSql } from 'postgres'; +import { AppError } from '../../../lib/shared/errors/app-error'; import { buildAuthHeader, buildSecretBindings, parseSecretPayload, SecretPayloadError, type ConnectorSecretPayload, -} from '../../../convex/connector_credentials/auth_injection.ts'; +} from '../../core/connector_credentials/auth_injection.ts'; import { connectorBearerScheme, loadConnectorDefinitions, -} from '../../../convex/connector_credentials/connector_catalog.ts'; -import { withImapFromAddress } from '../../../convex/connector_credentials/imap_from_address.ts'; -import { maskPayload } from '../../../convex/connector_credentials/masking.ts'; -import { normalizeEndpointOrigin } from '../../../convex/connector_credentials/mutations.ts'; +} from '../../core/connector_credentials/connector_catalog.ts'; +import { withImapFromAddress } from '../../core/connector_credentials/imap_from_address.ts'; +import { maskPayload } from '../../core/connector_credentials/masking.ts'; +import { normalizeEndpointOrigin } from '../../core/connector_credentials/mutations.ts'; import { decryptSecret, encryptSecret, KeyRotatedError, type EncryptedSecret, -} from '../../../convex/lib/secret_box.ts'; -import { AppError } from '../../../lib/shared/errors/app-error'; +} from '../../core/lib/secret_box.ts'; import { toJson } from '../../db/sql.ts'; /** diff --git a/services/platform/backend/domains/connectors/bridge-routes.ts b/services/platform/backend/domains/connectors/bridge-routes.ts index d881f5ae26..011ac47210 100644 --- a/services/platform/backend/domains/connectors/bridge-routes.ts +++ b/services/platform/backend/domains/connectors/bridge-routes.ts @@ -3,14 +3,14 @@ import { createHash } from 'node:crypto'; import { Hono } from 'hono'; import type { Sql } from 'postgres'; -import { verifyHostcallToken } from '../../../convex/connectors/hostcall_token.ts'; -import { - bridgeConnectorStatusImpl, - runBridgeConnectorImpl, -} from '../../../convex/node_only/sandbox/connectors_bridge.ts'; import { findConnector } from '../../../lib/connectors/catalog.ts'; import { ConnectorError } from '../../../lib/connectors/errors.ts'; import { createLiveHost } from '../../../lib/connectors/live-host.ts'; +import { verifyHostcallToken } from '../../core/connectors/hostcall_token.ts'; +import { + bridgeConnectorStatusImpl, + runBridgeConnectorImpl, +} from '../../core/node_only/sandbox/connectors_bridge.ts'; import { resolveConnectorCredential } from '../connector_credentials/service.ts'; import { getSessionTokenByHash } from '../sandbox/sessions.ts'; import { runConnectorAction } from './service.ts'; diff --git a/services/platform/backend/domains/connectors/oauth-app-routes.ts b/services/platform/backend/domains/connectors/oauth-app-routes.ts index 0bd294e3b1..7ffc86b307 100644 --- a/services/platform/backend/domains/connectors/oauth-app-routes.ts +++ b/services/platform/backend/domains/connectors/oauth-app-routes.ts @@ -2,12 +2,12 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; -import { resolveCloudImportOauthRedirectUri } from '../../../convex/cloud_import/deployment_config.ts'; -import { MICROSOFT_CLOUD_IMPORT_SCOPES } from '../../../convex/cloud_import/providers.ts'; import { defineAbilityFor } from '../../../lib/permissions/ability.ts'; import type { Auth } from '../../auth/auth.ts'; import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; import { requireSession } from '../../auth/session.ts'; +import { resolveCloudImportOauthRedirectUri } from '../../core/cloud_import/deployment_config.ts'; +import { MICROSOFT_CLOUD_IMPORT_SCOPES } from '../../core/cloud_import/providers.ts'; import { CLOUD_IMPORT_APP_SLUGS, deleteOauthApp, diff --git a/services/platform/backend/domains/connectors/oauth-apps.test.ts b/services/platform/backend/domains/connectors/oauth-apps.test.ts index aa06b56706..160b5e7bda 100644 --- a/services/platform/backend/domains/connectors/oauth-apps.test.ts +++ b/services/platform/backend/domains/connectors/oauth-apps.test.ts @@ -1,7 +1,7 @@ import type { Sql } from 'postgres'; import { afterEach, describe, expect, test, vi } from 'vitest'; -import { encryptSecret } from '../../../convex/lib/secret_box.ts'; +import { encryptSecret } from '../../core/lib/secret_box.ts'; import { applyMicrosoftTenant, getCloudImportAppStatus, diff --git a/services/platform/backend/domains/connectors/oauth-apps.ts b/services/platform/backend/domains/connectors/oauth-apps.ts index f64dba049c..5f789b3e1d 100644 --- a/services/platform/backend/domains/connectors/oauth-apps.ts +++ b/services/platform/backend/domains/connectors/oauth-apps.ts @@ -1,15 +1,15 @@ import type { Sql, TransactionSql } from 'postgres'; import { z } from 'zod'; -import { resolveCloudImportOauthApp } from '../../../convex/cloud_import/deployment_config.ts'; -import type { CloudImportProvider } from '../../../convex/cloud_import/types.ts'; -import { maskSecret } from '../../../convex/connector_credentials/masking.ts'; -import { resolveOauthAppCredentials } from '../../../convex/http_connectors/deployment_config.ts'; +import { resolveCloudImportOauthApp } from '../../core/cloud_import/deployment_config.ts'; +import type { CloudImportProvider } from '../../core/cloud_import/types.ts'; +import { maskSecret } from '../../core/connector_credentials/masking.ts'; +import { resolveOauthAppCredentials } from '../../core/http_connectors/deployment_config.ts'; import { decryptSecret, encryptSecret, type EncryptedSecret, -} from '../../../convex/lib/secret_box.ts'; +} from '../../core/lib/secret_box.ts'; import { toJson } from '../../db/sql.ts'; import { createAuditLog } from '../audit_logs/service.ts'; diff --git a/services/platform/backend/domains/connectors/oauth-routes.ts b/services/platform/backend/domains/connectors/oauth-routes.ts index ab022a6999..9e59662893 100644 --- a/services/platform/backend/domains/connectors/oauth-routes.ts +++ b/services/platform/backend/domains/connectors/oauth-routes.ts @@ -1,8 +1,6 @@ import { Hono } from 'hono'; import type { Sql } from 'postgres'; -import { resolveConnectorSettingsUrl } from '../../../convex/http_connectors/deployment_config.ts'; -import { renderConnectorErrorPage } from '../../../convex/http_connectors/error_page.ts'; import { defineAbilityFor } from '../../../lib/permissions/ability.ts'; import type { Auth } from '../../auth/auth.ts'; import { @@ -10,6 +8,8 @@ import { requireOrganizationMember, } from '../../auth/membership.ts'; import { requireSession, type AuthEnv } from '../../auth/session.ts'; +import { resolveConnectorSettingsUrl } from '../../core/http_connectors/deployment_config.ts'; +import { renderConnectorErrorPage } from '../../core/http_connectors/error_page.ts'; import { completeOauth2, startOauth2 } from './oauth.ts'; /** diff --git a/services/platform/backend/domains/connectors/oauth.ts b/services/platform/backend/domains/connectors/oauth.ts index d93e6a1d07..1f884cec13 100644 --- a/services/platform/backend/domains/connectors/oauth.ts +++ b/services/platform/backend/domains/connectors/oauth.ts @@ -1,20 +1,20 @@ import type { Sql } from 'postgres'; -import { generatePkcePair } from '../../../convex/enterprise_sso/pkce.ts'; -import { buildAuthorizeUrl } from '../../../convex/http_connectors/authorize_url.ts'; +import { findConnector } from '../../../lib/connectors/catalog.ts'; +import { generatePkcePair } from '../../core/enterprise_sso/pkce.ts'; +import { buildAuthorizeUrl } from '../../core/http_connectors/authorize_url.ts'; import { oauthAppEnvPrefix, resolveConnectorSettingsUrl, resolveOauthRedirectUri, -} from '../../../convex/http_connectors/deployment_config.ts'; +} from '../../core/http_connectors/deployment_config.ts'; import { hashStateToken, isPlausibleStateToken, mintStateToken, OAUTH_STATE_TTL_MS, -} from '../../../convex/http_connectors/oauth_state.ts'; -import { exchangeAuthorizationCode } from '../../../convex/http_connectors/token_exchange.ts'; -import { findConnector } from '../../../lib/connectors/catalog.ts'; +} from '../../core/http_connectors/oauth_state.ts'; +import { exchangeAuthorizationCode } from '../../core/http_connectors/token_exchange.ts'; import { createCredential } from '../connector_credentials/service.ts'; import { applyMicrosoftTenant, diff --git a/services/platform/backend/domains/connectors/service.ts b/services/platform/backend/domains/connectors/service.ts index f66f1a3204..a46af0e373 100644 --- a/services/platform/backend/domains/connectors/service.ts +++ b/services/platform/backend/domains/connectors/service.ts @@ -1,14 +1,5 @@ import type { Sql } from 'postgres'; -import { signHostcallToken } from '../../../convex/connectors/hostcall_token.ts'; -import { - ingestEmails, - ingestSentEmails, - listMailboxMessages, - querySyncCursor, - syncMailbox, -} from '../../../convex/conversations/sync_mailbox.ts'; -import { codeRunnerForSession } from '../../../convex/node_only/sandbox/engine_exec_runner.ts'; import { loadConnectorDefinitions } from '../../../lib/connectors/catalog.ts'; import { executeConnectorAction, @@ -36,7 +27,16 @@ import { type CodeRunner, } from '../../../lib/engine/core/runner.ts'; import { nodeVmRunner } from '../../../lib/engine/runners/node-vm.ts'; -import { createCtxShim } from '../../lib/convex-shim.ts'; +import { signHostcallToken } from '../../core/connectors/hostcall_token.ts'; +import { + ingestEmails, + ingestSentEmails, + listMailboxMessages, + querySyncCursor, + syncMailbox, +} from '../../core/conversations/sync_mailbox.ts'; +import { codeRunnerForSession } from '../../core/node_only/sandbox/engine_exec_runner.ts'; +import { createCtxShim } from '../../lib/ctx-shim.ts'; import { evaluateApprovalGate } from '../approvals/gate.ts'; import { createAuditLog } from '../audit_logs/service.ts'; import { resolveConnectorCredential } from '../connector_credentials/service.ts'; diff --git a/services/platform/backend/domains/connectors/slack-events.ts b/services/platform/backend/domains/connectors/slack-events.ts index bedaeba715..dd118926fc 100644 --- a/services/platform/backend/domains/connectors/slack-events.ts +++ b/services/platform/backend/domains/connectors/slack-events.ts @@ -1,14 +1,14 @@ import { Hono } from 'hono'; import type { Sql } from 'postgres'; -import { resolveSlackSigningSecret } from '../../../convex/http_connectors/deployment_config.ts'; +import { DEFAULT_TRUSTED_PROXIES } from '../../../lib/shared/schemas/governance.ts'; +import { getString, isRecord } from '../../../lib/utils/type-utils.ts'; +import { resolveSlackSigningSecret } from '../../core/http_connectors/deployment_config.ts'; import { SLACK_MAX_BODY_BYTES, verifySlackSignature, -} from '../../../convex/http_connectors/slack_signature.ts'; -import { getClientIp } from '../../../convex/lib/utils/client_ip.ts'; -import { DEFAULT_TRUSTED_PROXIES } from '../../../lib/shared/schemas/governance.ts'; -import { getString, isRecord } from '../../../lib/utils/type-utils.ts'; +} from '../../core/http_connectors/slack_signature.ts'; +import { getClientIp } from '../../core/lib/utils/client_ip.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import { readGovernancePolicy } from '../../lib/org-config.ts'; import { diff --git a/services/platform/backend/domains/connectors/sso-reuse.ts b/services/platform/backend/domains/connectors/sso-reuse.ts index f925d8bae1..43008fb4fa 100644 --- a/services/platform/backend/domains/connectors/sso-reuse.ts +++ b/services/platform/backend/domains/connectors/sso-reuse.ts @@ -3,7 +3,7 @@ import type { Sql } from 'postgres'; import { EntraIssuerError, extractTenantId, -} from '../../../convex/enterprise_sso/entra_id/constants.ts'; +} from '../../core/enterprise_sso/entra_id/constants.ts'; import { readSsoConnection, readSsoSecrets } from '../sso/config.ts'; /** diff --git a/services/platform/backend/domains/control/service.ts b/services/platform/backend/domains/control/service.ts index 043f83e3d1..a213697d66 100644 --- a/services/platform/backend/domains/control/service.ts +++ b/services/platform/backend/domains/control/service.ts @@ -2,11 +2,11 @@ import { transactSerializable } from '@tale/shared/db/serializable'; import { hashPassword } from 'better-auth/crypto'; import type { Sql } from 'postgres'; -import { normalizeAuthEmail } from '../../../convex/lib/auth/normalize_auth_email.ts'; import { isPasswordValid, passwordPolicyViolations, } from '../../../lib/shared/schemas/password.ts'; +import { normalizeAuthEmail } from '../../core/lib/auth/normalize_auth_email.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import { scaffoldNewOrganization } from '../organizations/scaffold.ts'; import { recordPasswordChange } from '../users/service.ts'; diff --git a/services/platform/backend/domains/conversations/routing.ts b/services/platform/backend/domains/conversations/routing.ts index a7a2da91c3..885d182fa7 100644 --- a/services/platform/backend/domains/conversations/routing.ts +++ b/services/platform/backend/domains/conversations/routing.ts @@ -1,6 +1,6 @@ import type { TransactionSql } from 'postgres'; -import { inboundRecipientAddress } from '../../../convex/conversations/reply_from.ts'; +import { inboundRecipientAddress } from '../../core/conversations/reply_from.ts'; import { readGovernancePolicyForOrg } from '../../lib/org-config.ts'; import { emitHintInTx } from '../../realtime/outbox.ts'; import { createAuditLog } from '../audit_logs/service.ts'; diff --git a/services/platform/backend/domains/conversations/search-chat.ts b/services/platform/backend/domains/conversations/search-chat.ts index 012ec00878..7de6da8c4c 100644 --- a/services/platform/backend/domains/conversations/search-chat.ts +++ b/services/platform/backend/domains/conversations/search-chat.ts @@ -1,10 +1,10 @@ import type { Sql } from 'postgres'; -import { conversationAssignmentAllows } from '../../../convex/lib/rls/helpers/conversation_assignment.ts'; -import { rowMatches } from '../../../convex/lib/search/relevance.ts'; -import { contactsSearchStrategy } from '../../../convex/lib/search/strategies/contacts.ts'; import { htmlToText } from '../../../lib/knowledge/html-to-text.ts'; import { getUserTeamIds } from '../../auth/membership.ts'; +import { conversationAssignmentAllows } from '../../core/lib/rls/helpers/conversation_assignment.ts'; +import { rowMatches } from '../../core/lib/search/relevance.ts'; +import { contactsSearchStrategy } from '../../core/lib/search/strategies/contacts.ts'; import { viewerIsAdmin } from './service.ts'; /** diff --git a/services/platform/backend/domains/conversations/send.ts b/services/platform/backend/domains/conversations/send.ts index 473e52deaa..33e5561ed3 100644 --- a/services/platform/backend/domains/conversations/send.ts +++ b/services/platform/backend/domains/conversations/send.ts @@ -1,20 +1,20 @@ import type { Sql, TransactionSql } from 'postgres'; -import { validateConversationAttachmentCaps } from '../../../convex/conversations/attachments.ts'; -import { buildThreadingHeaders } from '../../../convex/conversations/build_threading_headers.ts'; -import { sendConnectorAction } from '../../../convex/conversations/connector_slug.ts'; -import { inboundRecipientAddress } from '../../../convex/conversations/reply_from.ts'; +import { ConnectorError } from '../../../lib/connectors/errors.ts'; +import { nextConversationLastMessageAt } from '../../../lib/shared/conversations/message-order.ts'; +import { validateConversationAttachmentCaps } from '../../core/conversations/attachments.ts'; +import { buildThreadingHeaders } from '../../core/conversations/build_threading_headers.ts'; +import { sendConnectorAction } from '../../core/conversations/connector_slug.ts'; +import { inboundRecipientAddress } from '../../core/conversations/reply_from.ts'; import { BULK_REPLY_CAP, buildReplySubject, splitHtmlText, -} from '../../../convex/conversations/reply_to_conversation.ts'; +} from '../../core/conversations/reply_to_conversation.ts'; import { buildSendInput, externalIdFromSendOutput, -} from '../../../convex/conversations/send_input.ts'; -import { ConnectorError } from '../../../lib/connectors/errors.ts'; -import { nextConversationLastMessageAt } from '../../../lib/shared/conversations/message-order.ts'; +} from '../../core/conversations/send_input.ts'; import { toJson } from '../../db/sql.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import type { TaskPayloads } from '../../jobs/tasks.ts'; diff --git a/services/platform/backend/domains/conversations/service.ts b/services/platform/backend/domains/conversations/service.ts index 5953ffaedd..760b66ca9d 100644 --- a/services/platform/backend/domains/conversations/service.ts +++ b/services/platform/backend/domains/conversations/service.ts @@ -1,9 +1,9 @@ import type { Sql, TransactionSql } from 'postgres'; -import { conversationAssignmentAllows } from '../../../convex/lib/rls/helpers/conversation_assignment.ts'; import { projectConversationItem } from '../../../lib/shared/conversations/conversation-item.ts'; import { nextConversationLastMessageAt } from '../../../lib/shared/conversations/message-order.ts'; import { getUserTeamIds } from '../../auth/membership.ts'; +import { conversationAssignmentAllows } from '../../core/lib/rls/helpers/conversation_assignment.ts'; import { toJson } from '../../db/sql.ts'; import { emitHintInTx } from '../../realtime/outbox.ts'; import { createAuditLog } from '../audit_logs/service.ts'; diff --git a/services/platform/backend/domains/deployment/service.ts b/services/platform/backend/domains/deployment/service.ts index 94282584f0..14d308f38d 100644 --- a/services/platform/backend/domains/deployment/service.ts +++ b/services/platform/backend/domains/deployment/service.ts @@ -2,8 +2,21 @@ import { unlink } from 'node:fs/promises'; import type { Sql } from 'postgres'; -import { decideInstanceAdmin } from '../../../convex/deployment/auth_policy.ts'; -import { isDeploymentEditor } from '../../../convex/deployment/editors.ts'; +import { checkProviderHostPolicy } from '../../../lib/net/host-policy.ts'; +import { SafeFetchError, safeFetch } from '../../../lib/net/safe-fetch.ts'; +import type { + DeploymentConfig, + DeploymentSecretKey, +} from '../../../lib/shared/schemas/deployment.ts'; +import { + DEPLOYMENT_CONFIG_VERSION, + DEPLOYMENT_SECRET_KEYS, + convexStorageSchema, + deploymentConfigSchema, + pgConnectionSchema, +} from '../../../lib/shared/schemas/deployment.ts'; +import { decideInstanceAdmin } from '../../core/deployment/auth_policy.ts'; +import { isDeploymentEditor } from '../../core/deployment/editors.ts'; import { MAX_FILE_SIZE_BYTES, PREVIEWABLE_DEPLOYMENT_SECRET_KEYS, @@ -14,43 +27,27 @@ import { resolveDeploymentSecretsPath, resolveLegacyDeploymentConfigPath, serializeDeploymentConfig, -} from '../../../convex/deployment/file_utils.ts'; +} from '../../core/deployment/file_utils.ts'; import { UndecryptableExistingSecretError, prepareMergedDeploymentSecrets, -} from '../../../convex/deployment/secret_io.ts'; -import { testDatastoreConnection } from '../../../convex/deployment/test_datastore_connection.ts'; +} from '../../core/deployment/secret_io.ts'; +import { testDatastoreConnection } from '../../core/deployment/test_datastore_connection.ts'; import { atomicWrite, atomicWriteSecret, errnoCode, readJsonFile, sha256, -} from '../../../convex/lib/file_io.ts'; -import { checkProviderHostPolicy } from '../../../convex/lib/http/host_policy.ts'; -import { - SafeFetchError, - safeFetch, -} from '../../../convex/lib/http/safe_fetch.ts'; +} from '../../core/lib/file_io.ts'; import { EncryptedFileWithoutKeyError, decryptSecretsFile, encryptJsonWithSops, hasSopsKey, invalidateSecretsCache, -} from '../../../convex/lib/sops.ts'; -import { sanitizeError } from '../../../convex/lib/utils/sanitize_secrets.ts'; -import type { - DeploymentConfig, - DeploymentSecretKey, -} from '../../../lib/shared/schemas/deployment.ts'; -import { - DEPLOYMENT_CONFIG_VERSION, - DEPLOYMENT_SECRET_KEYS, - convexStorageSchema, - deploymentConfigSchema, - pgConnectionSchema, -} from '../../../lib/shared/schemas/deployment.ts'; +} from '../../core/lib/sops.ts'; +import { sanitizeError } from '../../core/lib/utils/sanitize_secrets.ts'; import { createAuditLog } from '../audit_logs/service.ts'; /** diff --git a/services/platform/backend/domains/documents/project-text.ts b/services/platform/backend/domains/documents/project-text.ts index 8c667fc3ed..518f22c51b 100644 --- a/services/platform/backend/domains/documents/project-text.ts +++ b/services/platform/backend/domains/documents/project-text.ts @@ -1,9 +1,9 @@ import type { Sql } from 'postgres'; -import { parseYamlMap } from '../../../convex/documents/parse_yaml_map.ts'; -import { serializeYamlMap } from '../../../convex/documents/serialize_yaml_map.ts'; -import { parseBlobRef } from '../../../convex/lib/storage/blob_ref.ts'; -import { s3GetObjectBytes } from '../../../convex/lib/storage/object_store.ts'; +import { parseYamlMap } from '../../core/documents/parse_yaml_map.ts'; +import { serializeYamlMap } from '../../core/documents/serialize_yaml_map.ts'; +import { parseBlobRef } from '../../core/lib/storage/blob_ref.ts'; +import { s3GetObjectBytes } from '../../core/lib/storage/object_store.ts'; import { resolveObjectStore } from '../../lib/object-store.ts'; import { resolveOrgSlug } from '../../lib/org-config.ts'; import { putOrgBlobBytes, registerUploadedBytes } from '../files/service.ts'; diff --git a/services/platform/backend/domains/documents/records.ts b/services/platform/backend/domains/documents/records.ts index 3206b6e8ee..afde88073b 100644 --- a/services/platform/backend/domains/documents/records.ts +++ b/services/platform/backend/domains/documents/records.ts @@ -1,6 +1,6 @@ import type { Sql, TransactionSql } from 'postgres'; -import { checkProjectAccess } from '../../../convex/projects/access.ts'; +import { checkProjectAccess } from '../../core/projects/access.ts'; import { toJson } from '../../db/sql.ts'; import { emitHintInTx } from '../../realtime/outbox.ts'; import { createAuditLog } from '../audit_logs/service.ts'; diff --git a/services/platform/backend/domains/documents/replacement.ts b/services/platform/backend/domains/documents/replacement.ts index ebea65e436..57b92c3586 100644 --- a/services/platform/backend/domains/documents/replacement.ts +++ b/services/platform/backend/domains/documents/replacement.ts @@ -2,22 +2,22 @@ import { createHash, randomUUID } from 'node:crypto'; import type { Sql, TransactionSql } from 'postgres'; -import { attestDocumentContentType } from '../../../convex/documents/attest_document_bytes.ts'; +import { + DOCUMENT_MAX_FILE_SIZE, + isRagIndexableFile, +} from '../../../lib/shared/file-types.ts'; +import { attestDocumentContentType } from '../../core/documents/attest_document_bytes.ts'; import { putImmutableS3Blob, s3BlobSize, -} from '../../../convex/lib/storage/blob_access.ts'; +} from '../../core/lib/storage/blob_access.ts'; import { encodeS3Ref, parseBlobRef, s3KeyBelongsToOrg, -} from '../../../convex/lib/storage/blob_ref.ts'; -import { s3GetObjectBytes } from '../../../convex/lib/storage/object_store.ts'; -import { checkProjectAccess } from '../../../convex/projects/access.ts'; -import { - DOCUMENT_MAX_FILE_SIZE, - isRagIndexableFile, -} from '../../../lib/shared/file-types.ts'; +} from '../../core/lib/storage/blob_ref.ts'; +import { s3GetObjectBytes } from '../../core/lib/storage/object_store.ts'; +import { checkProjectAccess } from '../../core/projects/access.ts'; import { toJson } from '../../db/sql.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import { diff --git a/services/platform/backend/domains/documents/service.ts b/services/platform/backend/domains/documents/service.ts index 59687a3beb..8be8d012ef 100644 --- a/services/platform/backend/domains/documents/service.ts +++ b/services/platform/backend/domains/documents/service.ts @@ -1,13 +1,13 @@ import type { Sql, TransactionSql } from 'postgres'; -import { hasTeamAccess } from '../../../convex/lib/team_access.ts'; -import { checkProjectAccess } from '../../../convex/projects/access.ts'; import { DOCUMENT_MAX_FILE_SIZE, DOCUMENT_UPLOAD_ALLOWED_EXTENSIONS, isAllowedDocumentUpload, resolveFileType, } from '../../../lib/shared/file-types.ts'; +import { hasTeamAccess } from '../../core/lib/team_access.ts'; +import { checkProjectAccess } from '../../core/projects/access.ts'; import { toJson } from '../../db/sql.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import { diff --git a/services/platform/backend/domains/feedback/service.ts b/services/platform/backend/domains/feedback/service.ts index 07511dcc2b..9e3de611bd 100644 --- a/services/platform/backend/domains/feedback/service.ts +++ b/services/platform/backend/domains/feedback/service.ts @@ -214,8 +214,7 @@ export async function getFeedbackStats( provider?: string; }, ): Promise> { - const { computeFeedbackStats } = - await import('../../../convex/feedback/stats.ts'); + const { computeFeedbackStats } = await import('../../core/feedback/stats.ts'); const now = Date.now(); const cutoffMs = args.periodDays !== undefined ? now - args.periodDays * DAY_MS : null; diff --git a/services/platform/backend/domains/file_metadata/watchdogs.ts b/services/platform/backend/domains/file_metadata/watchdogs.ts index b76b0421c7..d6c8e3469c 100644 --- a/services/platform/backend/domains/file_metadata/watchdogs.ts +++ b/services/platform/backend/domains/file_metadata/watchdogs.ts @@ -3,7 +3,7 @@ import type { Sql } from 'postgres'; import { getKnowledgePoolForOrg, PRIVATE_KNOWLEDGE_SCHEMA, -} from '../../../convex/knowledge/pool.ts'; +} from '../../core/knowledge/pool.ts'; import { emitHintInTx } from '../../realtime/outbox.ts'; /** diff --git a/services/platform/backend/domains/files/sandbox-blob-routes.ts b/services/platform/backend/domains/files/sandbox-blob-routes.ts index 730e87b5e8..aa8a45b3cd 100644 --- a/services/platform/backend/domains/files/sandbox-blob-routes.ts +++ b/services/platform/backend/domains/files/sandbox-blob-routes.ts @@ -5,8 +5,8 @@ import { isS3Ref, parseBlobRef, s3KeyBelongsToOrg, -} from '../../../convex/lib/storage/blob_ref.ts'; -import { verifyStageToken } from '../../../convex/lib/storage/sandbox_stage_token.ts'; +} from '../../core/lib/storage/blob_ref.ts'; +import { verifyStageToken } from '../../core/lib/storage/sandbox_stage_token.ts'; import { resolveObjectStore, s3PresignGetUrl } from '../../lib/object-store.ts'; import { resolveOrgSlug } from '../../lib/org-config.ts'; diff --git a/services/platform/backend/domains/files/service.ts b/services/platform/backend/domains/files/service.ts index 558034c0cb..7fdaadc88f 100644 --- a/services/platform/backend/domains/files/service.ts +++ b/services/platform/backend/domains/files/service.ts @@ -1,11 +1,8 @@ import type { Sql, TransactionSql } from 'postgres'; -import { - encodeS3Ref, - parseBlobRef, -} from '../../../convex/lib/storage/blob_ref.ts'; -import { s3KeyBelongsToOrg } from '../../../convex/lib/storage/blob_ref.ts'; -import { browserFacing } from '../../../convex/lib/storage/object_store.ts'; +import { encodeS3Ref, parseBlobRef } from '../../core/lib/storage/blob_ref.ts'; +import { s3KeyBelongsToOrg } from '../../core/lib/storage/blob_ref.ts'; +import { browserFacing } from '../../core/lib/storage/object_store.ts'; import { buildObjectKey, resolveObjectStore, diff --git a/services/platform/backend/domains/files/transcription.ts b/services/platform/backend/domains/files/transcription.ts index 1785f987f2..8d94cdf299 100644 --- a/services/platform/backend/domains/files/transcription.ts +++ b/services/platform/backend/domains/files/transcription.ts @@ -1,18 +1,18 @@ import type { Sql, TransactionSql } from 'postgres'; -import { transcribeAudioImpl } from '../../../convex/file_metadata/transcribe_audio.ts'; -import { pickExtensionFromMime } from '../../../convex/file_metadata/transcribe_dictation.ts'; -import { requestTranscription } from '../../../convex/file_metadata/transcription_request.ts'; -import { estimateTranscriptionCostCents } from '../../../convex/governance/cost_estimation.ts'; -import { checkProviderHostPolicy } from '../../../convex/lib/http/host_policy.ts'; -import { resolveTranscriptionModel } from '../../../convex/lib/providers/resolve_transcription_model.ts'; +import { checkProviderHostPolicy } from '../../../lib/net/host-policy.ts'; import { TRANSCRIPTION_SLUG } from '../../../lib/shared/constants/usage.ts'; +import { transcribeAudioImpl } from '../../core/file_metadata/transcribe_audio.ts'; +import { pickExtensionFromMime } from '../../core/file_metadata/transcribe_dictation.ts'; +import { requestTranscription } from '../../core/file_metadata/transcription_request.ts'; +import { estimateTranscriptionCostCents } from '../../core/governance/cost_estimation.ts'; +import { resolveTranscriptionModel } from '../../core/lib/providers/resolve_transcription_model.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import { createCtxShim, type ShimHandlers, type ShimScheduler, -} from '../../lib/convex-shim.ts'; +} from '../../lib/ctx-shim.ts'; import { chatShimHandlers } from '../chat/shim.ts'; import { incrementUsageLedger } from '../governance/service.ts'; import { heartbeatJobByStorageRef } from '../video_links/service.ts'; diff --git a/services/platform/backend/domains/folders/service.ts b/services/platform/backend/domains/folders/service.ts index fcc53e0037..d04ffa6d06 100644 --- a/services/platform/backend/domains/folders/service.ts +++ b/services/platform/backend/domains/folders/service.ts @@ -1,7 +1,7 @@ import type { Sql, TransactionSql } from 'postgres'; -import { hasTeamAccess } from '../../../convex/lib/team_access.ts'; -import { checkProjectAccess } from '../../../convex/projects/access.ts'; +import { hasTeamAccess } from '../../core/lib/team_access.ts'; +import { checkProjectAccess } from '../../core/projects/access.ts'; import { emitHintInTx } from '../../realtime/outbox.ts'; import { assertNotHeld } from '../legal_holds/service.ts'; import { diff --git a/services/platform/backend/domains/google_drive/routes.ts b/services/platform/backend/domains/google_drive/routes.ts index 95f93ae142..c754790a84 100644 --- a/services/platform/backend/domains/google_drive/routes.ts +++ b/services/platform/backend/domains/google_drive/routes.ts @@ -2,11 +2,11 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; -import { importFiles } from '../../../convex/google_drive/import_files.ts'; -import { listFiles } from '../../../convex/google_drive/list_files.ts'; import type { Auth } from '../../auth/auth.ts'; import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; import { requireSession } from '../../auth/session.ts'; +import { importFiles } from '../../core/google_drive/import_files.ts'; +import { listFiles } from '../../core/google_drive/list_files.ts'; import { SyncConfigError } from '../onedrive/service.ts'; import { cancelSyncConfig, diff --git a/services/platform/backend/domains/google_drive/service.ts b/services/platform/backend/domains/google_drive/service.ts index a7bbe18b7d..f5c40d75e0 100644 --- a/services/platform/backend/domains/google_drive/service.ts +++ b/services/platform/backend/domains/google_drive/service.ts @@ -1,8 +1,8 @@ import type { Sql, TransactionSql } from 'postgres'; -import { getFileMetadata } from '../../../convex/google_drive/get_file_metadata.ts'; -import { importFiles } from '../../../convex/google_drive/import_files.ts'; -import { listFolderContents } from '../../../convex/google_drive/list_folder_contents.ts'; +import { getFileMetadata } from '../../core/google_drive/get_file_metadata.ts'; +import { importFiles } from '../../core/google_drive/import_files.ts'; +import { listFolderContents } from '../../core/google_drive/list_folder_contents.ts'; import { resolveCloudAccessToken } from '../cloud_import/service.ts'; import { cancelSyncConfigRow, diff --git a/services/platform/backend/domains/governance/routes.ts b/services/platform/backend/domains/governance/routes.ts index d585e1540e..a7ca6af42f 100644 --- a/services/platform/backend/domains/governance/routes.ts +++ b/services/platform/backend/domains/governance/routes.ts @@ -3,14 +3,6 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; -import { - collectAllApplicableRules, - collectWarnings, - resolveEffectiveLimits, - type BudgetWarning, -} from '../../../convex/governance/budget_enforcement.ts'; -import { buildPeriodKey } from '../../../convex/governance/helpers.ts'; -import { isAdmin } from '../../../convex/lib/rls/helpers/role_helpers.ts'; import { dsarGovernanceConfigSchema, isFilePolicyType, @@ -20,6 +12,14 @@ import type { Auth } from '../../auth/auth.ts'; import { getUserTeamIds } from '../../auth/membership.ts'; import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; import { requireSession } from '../../auth/session.ts'; +import { + collectAllApplicableRules, + collectWarnings, + resolveEffectiveLimits, + type BudgetWarning, +} from '../../core/governance/budget_enforcement.ts'; +import { buildPeriodKey } from '../../core/governance/helpers.ts'; +import { isAdmin } from '../../core/lib/rls/helpers/role_helpers.ts'; import { readGovernancePolicyForOrg, resolveOrgSlug, diff --git a/services/platform/backend/domains/governance/service.ts b/services/platform/backend/domains/governance/service.ts index 119122478e..3108908bcd 100644 --- a/services/platform/backend/domains/governance/service.ts +++ b/services/platform/backend/domains/governance/service.ts @@ -1,20 +1,20 @@ import type { Sql, TransactionSql } from 'postgres'; +import { + getUserTeamIds, + findOrganizationMember, +} from '../../auth/membership.ts'; import { evaluateFeatureFlags, type ResolvedFeatureFlags, -} from '../../../convex/governance/feature_enforcement.ts'; -import { buildPeriodKeyFromTimestamp } from '../../../convex/governance/helpers.ts'; +} from '../../core/governance/feature_enforcement.ts'; +import { buildPeriodKeyFromTimestamp } from '../../core/governance/helpers.ts'; import { evaluateModelAccess, filterAccessibleModels, type ModelAccessCheckResult, -} from '../../../convex/governance/model_access_enforcement.ts'; -import { findApplicableModelRule } from '../../../convex/governance/resolve_default_model.ts'; -import { - getUserTeamIds, - findOrganizationMember, -} from '../../auth/membership.ts'; +} from '../../core/governance/model_access_enforcement.ts'; +import { findApplicableModelRule } from '../../core/governance/resolve_default_model.ts'; import { readGovernancePolicyForOrg } from '../../lib/org-config.ts'; /** diff --git a/services/platform/backend/domains/governance/session-idle.ts b/services/platform/backend/domains/governance/session-idle.ts index 64f7be036f..6ada9063d6 100644 --- a/services/platform/backend/domains/governance/session-idle.ts +++ b/services/platform/backend/domains/governance/session-idle.ts @@ -1,10 +1,10 @@ import type { Sql } from 'postgres'; -import { shouldRevokeIdleSession } from '../../../convex/governance/session_idle_enforcement.ts'; import { parseSessionIdleTimeoutMinutes, resolveEffectiveIdleMinutes, } from '../../../lib/shared/session-idle.ts'; +import { shouldRevokeIdleSession } from '../../core/governance/session_idle_enforcement.ts'; import { readGovernancePolicyForOrg } from '../../lib/org-config.ts'; import { createAuditLog } from '../audit_logs/service.ts'; diff --git a/services/platform/backend/domains/governance/settings-tail.ts b/services/platform/backend/domains/governance/settings-tail.ts index 372136434e..f8d2b246a0 100644 --- a/services/platform/backend/domains/governance/settings-tail.ts +++ b/services/platform/backend/domains/governance/settings-tail.ts @@ -1,15 +1,12 @@ import type { Sql, TransactionSql } from 'postgres'; -import { isLoosening } from '../../../convex/governance/dsar_policy.ts'; -import { - decryptSecret, - encryptSecret, -} from '../../../convex/lib/secret_box.ts'; import { dsarGovernanceConfigSchema, DEFAULT_DSAR_GOVERNANCE, type DsarGovernanceConfig, } from '../../../lib/shared/schemas/governance.ts'; +import { isLoosening } from '../../core/governance/dsar_policy.ts'; +import { decryptSecret, encryptSecret } from '../../core/lib/secret_box.ts'; import { toJson } from '../../db/sql.ts'; import { writeGovernancePolicyFile } from '../../lib/governance-policy-write.ts'; import { diff --git a/services/platform/backend/domains/governance/usage-metrics.ts b/services/platform/backend/domains/governance/usage-metrics.ts index 5486128c1d..a4875f48c5 100644 --- a/services/platform/backend/domains/governance/usage-metrics.ts +++ b/services/platform/backend/domains/governance/usage-metrics.ts @@ -6,7 +6,7 @@ import { type GetOrgUsageMetricsArgs, type OrgUsageMetrics, type UsageLedgerFoldRow, -} from '../../../convex/governance/get_org_usage_metrics.ts'; +} from '../../core/governance/get_org_usage_metrics.ts'; /** * The usage metrics page's read — the 0.4 fold REUSED over one bounded SQL diff --git a/services/platform/backend/domains/identities/service.ts b/services/platform/backend/domains/identities/service.ts index 6cda29c0c6..0526e6dc27 100644 --- a/services/platform/backend/domains/identities/service.ts +++ b/services/platform/backend/domains/identities/service.ts @@ -3,7 +3,7 @@ import type { Sql, TransactionSql } from 'postgres'; import { buildExternalOwnerId, isExternalOwnerId, -} from '../../../convex/identities/external_identities_helpers.ts'; +} from '../../core/identities/external_identities_helpers.ts'; /** * External author identities — the 0.4 `identities/external_identities.ts` diff --git a/services/platform/backend/domains/knowledge/admin.ts b/services/platform/backend/domains/knowledge/admin.ts index 9ff03bbee0..c0cd860292 100644 --- a/services/platform/backend/domains/knowledge/admin.ts +++ b/services/platform/backend/domains/knowledge/admin.ts @@ -1,18 +1,31 @@ import { mkdir } from 'node:fs/promises'; import path from 'node:path'; +import { checkProviderHostPolicy } from '../../../lib/net/host-policy.ts'; +import { pickEmbeddingRecommendations } from '../../../lib/shared/providers/embedding_recommendations.ts'; +import { zodErrorMessage } from '../../../lib/shared/schemas/format-error.ts'; +import { + KNOWLEDGE_CONNECTION_KEY, + KNOWLEDGE_EMBEDDING_KEY, + knowledgeConnectionSchema, + knowledgeConnectionSecretsSchema, + knowledgeEmbeddingSchema, + type KnowledgeConnection, + type KnowledgeConnectionSecrets, + type KnowledgeEmbeddingConfig, +} from '../../../lib/shared/schemas/knowledge.ts'; import { testDatastoreConnection, type DatastoreTestResult, -} from '../../../convex/deployment/test_datastore_connection.ts'; +} from '../../core/deployment/test_datastore_connection.ts'; import { connectionFilePath, connectionSecretsFilePath, embeddingFilePath, knowledgeConfigDir, readPassword, -} from '../../../convex/knowledge/connection.ts'; -import { invalidateOrgUrl } from '../../../convex/knowledge/pool.ts'; +} from '../../core/knowledge/connection.ts'; +import { invalidateOrgUrl } from '../../core/knowledge/pool.ts'; import { atomicWrite, atomicWriteSecret, @@ -22,27 +35,14 @@ import { removeDirSafe, removeFileSafe, safeJoinWithinDir, -} from '../../../convex/lib/file_io.ts'; -import { checkProviderHostPolicy } from '../../../convex/lib/http/host_policy.ts'; -import { getProviderCatalog } from '../../../convex/lib/providers/catalog_fetch.ts'; -import { resolveProvidersForOrg } from '../../../convex/lib/providers/org_providers.ts'; +} from '../../core/lib/file_io.ts'; +import { getProviderCatalog } from '../../core/lib/providers/catalog_fetch.ts'; +import { resolveProvidersForOrg } from '../../core/lib/providers/org_providers.ts'; import { encryptJsonWithSops, hasSopsKey, invalidateSecretsCache, -} from '../../../convex/lib/sops.ts'; -import { pickEmbeddingRecommendations } from '../../../lib/shared/providers/embedding_recommendations.ts'; -import { zodErrorMessage } from '../../../lib/shared/schemas/format-error.ts'; -import { - KNOWLEDGE_CONNECTION_KEY, - KNOWLEDGE_EMBEDDING_KEY, - knowledgeConnectionSchema, - knowledgeConnectionSecretsSchema, - knowledgeEmbeddingSchema, - type KnowledgeConnection, - type KnowledgeConnectionSecrets, - type KnowledgeEmbeddingConfig, -} from '../../../lib/shared/schemas/knowledge.ts'; +} from '../../core/lib/sops.ts'; /** * The knowledge-DB + embedding ADMIN config (the 0.4 `knowledge/actions` + diff --git a/services/platform/backend/domains/knowledge/service.ts b/services/platform/backend/domains/knowledge/service.ts index 341890c13a..2887e743c5 100644 --- a/services/platform/backend/domains/knowledge/service.ts +++ b/services/platform/backend/domains/knowledge/service.ts @@ -1,36 +1,36 @@ import type { Sql, TransactionSql } from 'postgres'; -import { readOrgEmbeddingConfig } from '../../../convex/knowledge/connection.ts'; -import { applyCorpusSchema } from '../../../convex/knowledge/ddl.ts'; -import { pinDimensions } from '../../../convex/knowledge/dimensions.ts'; +import { PRIVATE_KNOWLEDGE_SCHEMA } from '../../../lib/knowledge/types.ts'; +import { readOrgEmbeddingConfig } from '../../core/knowledge/connection.ts'; +import { applyCorpusSchema } from '../../core/knowledge/ddl.ts'; +import { pinDimensions } from '../../core/knowledge/dimensions.ts'; import { EmbeddingNotConfigured, embedderForOrg, -} from '../../../convex/knowledge/embedding.ts'; +} from '../../core/knowledge/embedding.ts'; import { fetchDocumentByFileId, type FetchDocumentByFileIdArgs, type FetchedDocument, -} from '../../../convex/knowledge/fetch.ts'; -import { indexDocument } from '../../../convex/knowledge/indexing.ts'; -import { parsePiiConfig } from '../../../convex/knowledge/pii_gate.ts'; +} from '../../core/knowledge/fetch.ts'; +import { indexDocument } from '../../core/knowledge/indexing.ts'; +import { parsePiiConfig } from '../../core/knowledge/pii_gate.ts'; import { getKnowledgePool, getKnowledgePoolForOrg, resolveOrgUrl, -} from '../../../convex/knowledge/pool.ts'; +} from '../../core/knowledge/pool.ts'; import { searchKnowledge, type SearchKnowledgeArgs, -} from '../../../convex/knowledge/search.ts'; +} from '../../core/knowledge/search.ts'; import { extractText, isSupported, -} from '../../../convex/lib/knowledge/extraction/router.ts'; -import { parseBlobRef } from '../../../convex/lib/storage/blob_ref.ts'; -import { s3GetObjectBytes } from '../../../convex/lib/storage/object_store.ts'; -import { PRIVATE_KNOWLEDGE_SCHEMA } from '../../../lib/knowledge/types.ts'; -import { createCtxShim, type ShimHandlers } from '../../lib/convex-shim.ts'; +} from '../../core/lib/knowledge/extraction/router.ts'; +import { parseBlobRef } from '../../core/lib/storage/blob_ref.ts'; +import { s3GetObjectBytes } from '../../core/lib/storage/object_store.ts'; +import { createCtxShim, type ShimHandlers } from '../../lib/ctx-shim.ts'; import { resolveObjectStore } from '../../lib/object-store.ts'; import { readGovernancePolicy, resolveOrgSlug } from '../../lib/org-config.ts'; import { emitHintInTx } from '../../realtime/outbox.ts'; diff --git a/services/platform/backend/domains/knowledge_entries/service.ts b/services/platform/backend/domains/knowledge_entries/service.ts index 2d718b19fa..6f9a411631 100644 --- a/services/platform/backend/domains/knowledge_entries/service.ts +++ b/services/platform/backend/domains/knowledge_entries/service.ts @@ -2,7 +2,7 @@ import { createHash } from 'node:crypto'; import type { Sql, TransactionSql } from 'postgres'; -import { validateTopicAndContent } from '../../../convex/knowledge_entries/helpers.ts'; +import { validateTopicAndContent } from '../../core/knowledge_entries/helpers.ts'; import { toJson } from '../../db/sql.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import { diff --git a/services/platform/backend/domains/login_attempts/service.ts b/services/platform/backend/domains/login_attempts/service.ts index 61f8a3c49b..90a5e3ef23 100644 --- a/services/platform/backend/domains/login_attempts/service.ts +++ b/services/platform/backend/domains/login_attempts/service.ts @@ -1,16 +1,16 @@ import type { Sql, TransactionSql } from 'postgres'; -import { normalizeAuthEmail } from '../../../convex/lib/auth/normalize_auth_email.ts'; +import { getUserOrganizations } from '../../auth/membership.ts'; +import { normalizeAuthEmail } from '../../core/lib/auth/normalize_auth_email.ts'; import { splitEmailForAudit, splitIpForAudit, -} from '../../../convex/lib/helpers/pii_hash.ts'; +} from '../../core/lib/helpers/pii_hash.ts'; import { computeLockedUntil, DEFAULT_LOGIN_POLICY, selectStrictestPolicy, -} from '../../../convex/login_attempts/helpers.ts'; -import { getUserOrganizations } from '../../auth/membership.ts'; +} from '../../core/login_attempts/helpers.ts'; import { readGovernancePolicyForOrg } from '../../lib/org-config.ts'; import { createAuditLog } from '../audit_logs/service.ts'; import { writeNotificationForOrgs } from '../notifications/service.ts'; diff --git a/services/platform/backend/domains/object_storage/bootstrap.ts b/services/platform/backend/domains/object_storage/bootstrap.ts index 7566794098..ceecf32cf1 100644 --- a/services/platform/backend/domains/object_storage/bootstrap.ts +++ b/services/platform/backend/domains/object_storage/bootstrap.ts @@ -24,13 +24,17 @@ import { mkdir } from 'node:fs/promises'; -import { atomicWrite, atomicWriteSecret } from '../../../convex/lib/file_io.ts'; +import { + resolveBundledObjectStore, + type BundledObjectStore, +} from '../../../lib/utils/bundled-object-store.ts'; +import { atomicWrite, atomicWriteSecret } from '../../core/lib/file_io.ts'; import { encryptJsonWithSops, hasSopsKey, invalidateSecretsCache, -} from '../../../convex/lib/sops.ts'; -import { buildS3ObjectStore } from '../../../convex/lib/storage/object_store.ts'; +} from '../../core/lib/sops.ts'; +import { buildS3ObjectStore } from '../../core/lib/storage/object_store.ts'; import { readOrgObjectStorageConnection, resolveObjectStorageConnectionFilePath, @@ -38,11 +42,7 @@ import { resolveObjectStorageDir, serializeObjectStorageConnectionJson, serializeObjectStorageSecretsJson, -} from '../../../convex/object_storage/file_utils.ts'; -import { - resolveBundledObjectStore, - type BundledObjectStore, -} from '../../../lib/utils/bundled-object-store.ts'; +} from '../../core/object_storage/file_utils.ts'; import { clearObjectStoreCache } from '../../lib/object-store.ts'; /** The config tree the deployment default lives under. */ diff --git a/services/platform/backend/domains/object_storage/service.ts b/services/platform/backend/domains/object_storage/service.ts index 88aa390c23..dc415b7901 100644 --- a/services/platform/backend/domains/object_storage/service.ts +++ b/services/platform/backend/domains/object_storage/service.ts @@ -3,6 +3,9 @@ import path from 'node:path'; import type { Sql } from 'postgres'; +import { checkProviderHostPolicy } from '../../../lib/net/host-policy.ts'; +import { zodErrorMessage } from '../../../lib/shared/schemas/format-error.ts'; +import { objectStorageConnectionFileSchema } from '../../../lib/shared/schemas/object_storage.ts'; import { atomicWrite, atomicWriteSecret, @@ -11,14 +14,13 @@ import { readFileSafe, removeDirSafe, removeFileSafe, -} from '../../../convex/lib/file_io.ts'; -import { checkProviderHostPolicy } from '../../../convex/lib/http/host_policy.ts'; +} from '../../core/lib/file_io.ts'; import { encryptJsonWithSops, hasSopsKey, invalidateSecretsCache, -} from '../../../convex/lib/sops.ts'; -import { parseBlobRef } from '../../../convex/lib/storage/blob_ref.ts'; +} from '../../core/lib/sops.ts'; +import { parseBlobRef } from '../../core/lib/storage/blob_ref.ts'; import { buildS3ObjectStore, invalidateOrgObjectStore, @@ -27,7 +29,7 @@ import { s3HeadObject, s3PutObject, type S3ObjectStore, -} from '../../../convex/lib/storage/object_store.ts'; +} from '../../core/lib/storage/object_store.ts'; import { parseObjectStorageConnectionJson, readObjectStorageSecrets, @@ -38,9 +40,7 @@ import { serializeObjectStorageConnectionJson, serializeObjectStorageSecretsJson, type ObjectStorageConnectionFile, -} from '../../../convex/object_storage/file_utils.ts'; -import { zodErrorMessage } from '../../../lib/shared/schemas/format-error.ts'; -import { objectStorageConnectionFileSchema } from '../../../lib/shared/schemas/object_storage.ts'; +} from '../../core/object_storage/file_utils.ts'; import { toJson } from '../../db/sql.ts'; import { clearObjectStoreCache } from '../../lib/object-store.ts'; diff --git a/services/platform/backend/domains/onedrive/routes.ts b/services/platform/backend/domains/onedrive/routes.ts index 45c619f077..898f6600c2 100644 --- a/services/platform/backend/domains/onedrive/routes.ts +++ b/services/platform/backend/domains/onedrive/routes.ts @@ -2,14 +2,14 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; -import { importFiles } from '../../../convex/onedrive/import_files.ts'; -import { listFiles } from '../../../convex/onedrive/list_files.ts'; -import { listSharePointDrives } from '../../../convex/onedrive/list_sharepoint_drives.ts'; -import { listSharePointFiles } from '../../../convex/onedrive/list_sharepoint_files.ts'; -import { listSharePointSites } from '../../../convex/onedrive/list_sharepoint_sites.ts'; import type { Auth } from '../../auth/auth.ts'; import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; import { requireSession } from '../../auth/session.ts'; +import { importFiles } from '../../core/onedrive/import_files.ts'; +import { listFiles } from '../../core/onedrive/list_files.ts'; +import { listSharePointDrives } from '../../core/onedrive/list_sharepoint_drives.ts'; +import { listSharePointFiles } from '../../core/onedrive/list_sharepoint_files.ts'; +import { listSharePointSites } from '../../core/onedrive/list_sharepoint_sites.ts'; import { checkOrganizationRateLimit } from '../../lib/rate-limit.ts'; import { cancelSyncConfig, diff --git a/services/platform/backend/domains/onedrive/service.ts b/services/platform/backend/domains/onedrive/service.ts index b8728b32d1..e81dd5a55d 100644 --- a/services/platform/backend/domains/onedrive/service.ts +++ b/services/platform/backend/domains/onedrive/service.ts @@ -1,28 +1,28 @@ import type { Sql, TransactionSql } from 'postgres'; +import { + isRagIndexableFile, + resolveFileType, +} from '../../../lib/shared/file-types.ts'; +import { isRecord } from '../../../lib/utils/type-utils.ts'; import { pickMicrosoftAccount, type MicrosoftAccountCandidate, -} from '../../../convex/accounts/microsoft_account.ts'; -import { extractExtension } from '../../../convex/documents/extract_extension.ts'; -import { extractTenantId } from '../../../convex/enterprise_sso/entra_id/constants.ts'; -import { sourceFromProvider } from '../../../convex/file_metadata/source_from_provider.ts'; -import { deleteKnowledgeDocument } from '../../../convex/legacy/knowledge_delete.ts'; -import { getFileMetadata } from '../../../convex/onedrive/get_file_metadata.ts'; -import { importFiles } from '../../../convex/onedrive/import_files.ts'; -import type { FileItem } from '../../../convex/onedrive/list_folder_contents.ts'; -import { listFolderContents } from '../../../convex/onedrive/list_folder_contents.ts'; +} from '../../core/accounts/microsoft_account.ts'; +import { extractExtension } from '../../core/documents/extract_extension.ts'; +import { extractTenantId } from '../../core/enterprise_sso/entra_id/constants.ts'; +import { sourceFromProvider } from '../../core/file_metadata/source_from_provider.ts'; +import { deleteKnowledgeDocument } from '../../core/legacy/knowledge_delete.ts'; +import { getFileMetadata } from '../../core/onedrive/get_file_metadata.ts'; +import { importFiles } from '../../core/onedrive/import_files.ts'; +import type { FileItem } from '../../core/onedrive/list_folder_contents.ts'; +import { listFolderContents } from '../../core/onedrive/list_folder_contents.ts'; import { buildSyncImportItems, selectDocumentsToPrune, type SyncedDocumentRef, -} from '../../../convex/onedrive/reconcile_folder_sync.ts'; -import { refreshToken as refreshMicrosoftLoginToken } from '../../../convex/onedrive/refresh_token.ts'; -import { - isRagIndexableFile, - resolveFileType, -} from '../../../lib/shared/file-types.ts'; -import { isRecord } from '../../../lib/utils/type-utils.ts'; +} from '../../core/onedrive/reconcile_folder_sync.ts'; +import { refreshToken as refreshMicrosoftLoginToken } from '../../core/onedrive/refresh_token.ts'; import { toJson } from '../../db/sql.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import { resolveOrgSlug } from '../../lib/org-config.ts'; diff --git a/services/platform/backend/domains/organizations/scaffold.ts b/services/platform/backend/domains/organizations/scaffold.ts index 5f2107df6d..1b5b28d2bf 100644 --- a/services/platform/backend/domains/organizations/scaffold.ts +++ b/services/platform/backend/domains/organizations/scaffold.ts @@ -1,4 +1,4 @@ -import { scaffoldOrgFromCatalog } from '../../../convex/organizations/scaffold.ts'; +import { scaffoldOrgFromCatalog } from '../../core/organizations/scaffold.ts'; /** * Org config-tree scaffolding — the worker-job face of the 0.4 scaffolder. diff --git a/services/platform/backend/domains/projects/secrets.ts b/services/platform/backend/domains/projects/secrets.ts index 5c4436f9cb..69a40cb47a 100644 --- a/services/platform/backend/domains/projects/secrets.ts +++ b/services/platform/backend/domains/projects/secrets.ts @@ -1,7 +1,7 @@ import type { Sql, TransactionSql } from 'postgres'; -import { encryptSecret } from '../../../convex/lib/secret_box.ts'; import { SECRET_NAME_RE } from '../../../lib/shared/schemas/secrets.ts'; +import { encryptSecret } from '../../core/lib/secret_box.ts'; import { toJson } from '../../db/sql.ts'; import { emitHintInTx } from '../../realtime/outbox.ts'; import { diff --git a/services/platform/backend/domains/projects/service.ts b/services/platform/backend/domains/projects/service.ts index abba1fb614..b10167b01d 100644 --- a/services/platform/backend/domains/projects/service.ts +++ b/services/platform/backend/domains/projects/service.ts @@ -1,25 +1,25 @@ import type { Sql, TransactionSql } from 'postgres'; +import { isHarnessSlug } from '../../../lib/harnesses/types.ts'; +import { + deriveProjectKey, + isValidProjectKey, + normalizeProjectKey, + PROJECT_KEY_MAX, +} from '../../../lib/shared/project_key.ts'; +import { getUserTeamIds } from '../../auth/membership.ts'; import { ADMIN_ROLES, checkProjectAccess, EDITOR_ROLES, isOrgWideProject, normalizeSharing, -} from '../../../convex/projects/access.ts'; +} from '../../core/projects/access.ts'; import { PROJECT_AUDIT_ACTIONS, PROJECT_RESOURCE_TYPE, -} from '../../../convex/projects/audit_actions.ts'; -import { normalizeToolGrants } from '../../../convex/sandbox/tool_names.ts'; -import { isHarnessSlug } from '../../../lib/harnesses/types.ts'; -import { - deriveProjectKey, - isValidProjectKey, - normalizeProjectKey, - PROJECT_KEY_MAX, -} from '../../../lib/shared/project_key.ts'; -import { getUserTeamIds } from '../../auth/membership.ts'; +} from '../../core/projects/audit_actions.ts'; +import { normalizeToolGrants } from '../../core/sandbox/tool_names.ts'; import { emitHintInTx } from '../../realtime/outbox.ts'; import { createAuditLog } from '../audit_logs/service.ts'; import { emitEvent } from '../events/emit.ts'; diff --git a/services/platform/backend/domains/provider_credentials/service.ts b/services/platform/backend/domains/provider_credentials/service.ts index 8c3afe9b37..09c517c08d 100644 --- a/services/platform/backend/domains/provider_credentials/service.ts +++ b/services/platform/backend/domains/provider_credentials/service.ts @@ -1,15 +1,15 @@ import type { Sql, TransactionSql } from 'postgres'; -import type { EncryptedSecret } from '../../../convex/lib/secret_box.ts'; -import { encryptSecret } from '../../../convex/lib/secret_box.ts'; -import { maskSecret } from '../../../convex/provider_credentials/masking.ts'; +import { isAdminOrDeveloperRole } from '../../auth/membership.ts'; +import type { EncryptedSecret } from '../../core/lib/secret_box.ts'; +import { encryptSecret } from '../../core/lib/secret_box.ts'; +import { maskSecret } from '../../core/provider_credentials/masking.ts'; import { resolveProviderCredential as resolveProviderCredential04, type ResolvedProviderCredential, -} from '../../../convex/provider_credentials/resolve_credential.ts'; -import { isAdminOrDeveloperRole } from '../../auth/membership.ts'; +} from '../../core/provider_credentials/resolve_credential.ts'; import { toJson } from '../../db/sql.ts'; -import { createCtxShim } from '../../lib/convex-shim.ts'; +import { createCtxShim } from '../../lib/ctx-shim.ts'; import { createAuditLog } from '../audit_logs/service.ts'; /** @@ -140,7 +140,7 @@ export async function resolveProviderCredential( ): Promise { const shim = createCtxShim(credentialShimHandlers(sql)); return resolveProviderCredential04( - // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the reused 0.4 resolver touches only runQuery (see convex-shim contract) + // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the reused 0.4 resolver touches only runQuery (see ctx-shim contract) shim as unknown as Parameters[0], { organizationId: args.organizationId, diff --git a/services/platform/backend/domains/providers/routes.ts b/services/platform/backend/domains/providers/routes.ts index 6af9fa572d..60b430ea0a 100644 --- a/services/platform/backend/domains/providers/routes.ts +++ b/services/platform/backend/domains/providers/routes.ts @@ -1,23 +1,23 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; -import { getProviderCatalog } from '../../../convex/lib/providers/catalog_fetch.ts'; -import { credentialAuthFor } from '../../../convex/lib/providers/credential_auth.ts'; +import type { Auth } from '../../auth/auth.ts'; +import { isAdminOrDeveloperRole } from '../../auth/membership.ts'; +import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; +import { requireSession } from '../../auth/session.ts'; +import { getProviderCatalog } from '../../core/lib/providers/catalog_fetch.ts'; +import { credentialAuthFor } from '../../core/lib/providers/credential_auth.ts'; import { deriveHarnessStatus, type SubscriptionCredentialFact, -} from '../../../convex/lib/providers/harness_status.ts'; +} from '../../core/lib/providers/harness_status.ts'; import { loadHarnesses, readSystemEntryIcon, -} from '../../../convex/lib/providers/load_system_config.ts'; -import { resolveProvidersForOrg } from '../../../convex/lib/providers/org_providers.ts'; -import { resolveOrgVisionModel } from '../../../convex/lib/providers/resolve_vision_model.ts'; -import type { Auth } from '../../auth/auth.ts'; -import { isAdminOrDeveloperRole } from '../../auth/membership.ts'; -import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; -import { requireSession } from '../../auth/session.ts'; -import { createCtxShim } from '../../lib/convex-shim.ts'; +} from '../../core/lib/providers/load_system_config.ts'; +import { resolveProvidersForOrg } from '../../core/lib/providers/org_providers.ts'; +import { resolveOrgVisionModel } from '../../core/lib/providers/resolve_vision_model.ts'; +import { createCtxShim } from '../../lib/ctx-shim.ts'; import { readGovernancePolicyForOrg } from '../../lib/org-config.ts'; import { resolveOrgSlug } from '../../lib/org-config.ts'; import { listComposerModels } from '../chat/composer.ts'; diff --git a/services/platform/backend/domains/provisioning/service.ts b/services/platform/backend/domains/provisioning/service.ts index 7fabec9b17..ea807b47e0 100644 --- a/services/platform/backend/domains/provisioning/service.ts +++ b/services/platform/backend/domains/provisioning/service.ts @@ -1,9 +1,9 @@ import type { Sql } from 'postgres'; -import { loadSeedablePacks } from '../../../convex/provisioning/provision_default_automations.ts'; import { automationPresentationSchema } from '../../../lib/shared/schemas/automation_presentation.ts'; import { automationSettingsSchema } from '../../../lib/shared/schemas/automation_settings.ts'; import { taskSubjectContractSchema } from '../../../lib/shared/schemas/task_contract.ts'; +import { loadSeedablePacks } from '../../core/provisioning/provision_default_automations.ts'; import { saveVersion, setTrigger } from '../automations/store.ts'; import { getProjectAuthContext } from '../projects/service.ts'; import { diff --git a/services/platform/backend/domains/retention/routes.ts b/services/platform/backend/domains/retention/routes.ts index 7e6ea44818..34991196b1 100644 --- a/services/platform/backend/domains/retention/routes.ts +++ b/services/platform/backend/domains/retention/routes.ts @@ -3,10 +3,19 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; +import { retentionPolicyConfigSchema } from '../../../lib/shared/schemas/governance.ts'; +import { + hashAppliedBounds, + type RetentionCategory, +} from '../../../lib/shared/schemas/retention.ts'; +import type { Auth } from '../../auth/auth.ts'; +import { isAdminRole } from '../../auth/membership.ts'; +import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; +import { requireSession } from '../../auth/session.ts'; import { buildImpactPreview, diffBounds, -} from '../../../convex/governance/retention_bounds_proposal.ts'; +} from '../../core/governance/retention_bounds_proposal.ts'; import { assertWithinBounds, buildBoundsByCategory, @@ -14,16 +23,7 @@ import { applyEnvTighteningAll, isRetentionDisabled, RetentionConfigMissingError, -} from '../../../convex/governance/retention_floors.ts'; -import { retentionPolicyConfigSchema } from '../../../lib/shared/schemas/governance.ts'; -import { - hashAppliedBounds, - type RetentionCategory, -} from '../../../lib/shared/schemas/retention.ts'; -import type { Auth } from '../../auth/auth.ts'; -import { isAdminRole } from '../../auth/membership.ts'; -import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; -import { requireSession } from '../../auth/session.ts'; +} from '../../core/governance/retention_floors.ts'; import { writeGovernancePolicyFile } from '../../lib/governance-policy-write.ts'; import { readGovernancePolicyForOrg, diff --git a/services/platform/backend/domains/retention/service.ts b/services/platform/backend/domains/retention/service.ts index 123bf412f9..0c3ffbbfc3 100644 --- a/services/platform/backend/domains/retention/service.ts +++ b/services/platform/backend/domains/retention/service.ts @@ -1,20 +1,20 @@ import type { Sql } from 'postgres'; -import { - applyEnvTighteningAll, - clampConfigToBounds, - isRetentionDisabled, - type EffectiveBoundDef, -} from '../../../convex/governance/retention_floors.ts'; -import { deleteKnowledgeDocument } from '../../../convex/legacy/knowledge_delete.ts'; -import { readDomainConfigFile } from '../../../convex/lib/config_store/read_domain_file.ts'; -import { getConfigRoot } from '../../../convex/lib/file_io.ts'; -import { parseBlobRef } from '../../../convex/lib/storage/blob_ref.ts'; import type { RetentionPolicyConfig } from '../../../lib/shared/schemas/governance.ts'; import { retentionDefaultsConfigSchema, type RetentionCategory, } from '../../../lib/shared/schemas/retention.ts'; +import { + applyEnvTighteningAll, + clampConfigToBounds, + isRetentionDisabled, + type EffectiveBoundDef, +} from '../../core/governance/retention_floors.ts'; +import { deleteKnowledgeDocument } from '../../core/legacy/knowledge_delete.ts'; +import { readDomainConfigFile } from '../../core/lib/config_store/read_domain_file.ts'; +import { getConfigRoot } from '../../core/lib/file_io.ts'; +import { parseBlobRef } from '../../core/lib/storage/blob_ref.ts'; import { toJson } from '../../db/sql.ts'; import { resolveObjectStore, s3DeleteObject } from '../../lib/object-store.ts'; import { diff --git a/services/platform/backend/domains/sandbox/dispatch-routes.ts b/services/platform/backend/domains/sandbox/dispatch-routes.ts index fde0d278a1..149df2b2d9 100644 --- a/services/platform/backend/domains/sandbox/dispatch-routes.ts +++ b/services/platform/backend/domains/sandbox/dispatch-routes.ts @@ -6,8 +6,8 @@ import type { Sql } from 'postgres'; import { dispatchWorkspaceToolImpl, workspaceToolStatusImpl, -} from '../../../convex/node_only/sandbox/workspace_tools_bridge.ts'; -import { createCtxShim } from '../../lib/convex-shim.ts'; +} from '../../core/node_only/sandbox/workspace_tools_bridge.ts'; +import { createCtxShim } from '../../lib/ctx-shim.ts'; import { getSessionTokenByHash } from './sessions.ts'; import { sandboxToolShimHandlers } from './shim.ts'; diff --git a/services/platform/backend/domains/sandbox/recovery.ts b/services/platform/backend/domains/sandbox/recovery.ts index 6198689095..bab7ca5f90 100644 --- a/services/platform/backend/domains/sandbox/recovery.ts +++ b/services/platform/backend/domains/sandbox/recovery.ts @@ -1,6 +1,6 @@ import type { Sql } from 'postgres'; -import { sessionOpLastSignOfLifeMs } from '../../../convex/sandbox/agent_deadline.ts'; +import { sessionOpLastSignOfLifeMs } from '../../core/sandbox/agent_deadline.ts'; /** * The agent-turn recovery primitives shared by the task and automation diff --git a/services/platform/backend/domains/sandbox/routes.ts b/services/platform/backend/domains/sandbox/routes.ts index 68a1b8f1dd..671a222501 100644 --- a/services/platform/backend/domains/sandbox/routes.ts +++ b/services/platform/backend/domains/sandbox/routes.ts @@ -3,18 +3,18 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; -import { sessionCancelExec } from '../../../convex/node_only/sandbox/helpers/session_client.ts'; -import { - DEFAULT_SANDBOX_QUOTA, - sessionBudgetForOwnerType, - sessionCapFor, - type SessionBudget, -} from '../../../convex/sandbox/quota_policy.ts'; import { defineAbilityFor } from '../../../lib/permissions/ability.ts'; import type { Auth } from '../../auth/auth.ts'; import { isAdminOrDeveloperRole } from '../../auth/membership.ts'; import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; import { requireSession } from '../../auth/session.ts'; +import { sessionCancelExec } from '../../core/node_only/sandbox/helpers/session_client.ts'; +import { + DEFAULT_SANDBOX_QUOTA, + sessionBudgetForOwnerType, + sessionCapFor, + type SessionBudget, +} from '../../core/sandbox/quota_policy.ts'; import { readGovernancePolicyForOrg } from '../../lib/org-config.ts'; import { pinSession, reconcileSession, teardownSession } from './service.ts'; import { diff --git a/services/platform/backend/domains/sandbox/service.ts b/services/platform/backend/domains/sandbox/service.ts index 561f157914..dc10b54984 100644 --- a/services/platform/backend/domains/sandbox/service.ts +++ b/services/platform/backend/domains/sandbox/service.ts @@ -7,7 +7,7 @@ import { sessionDestroy, sessionIsAlive, sessionSetPinned, -} from '../../../convex/node_only/sandbox/helpers/session_client.ts'; +} from '../../core/node_only/sandbox/helpers/session_client.ts'; import { getSessionBySessionId, markSessionDestroyed, diff --git a/services/platform/backend/domains/sandbox/sessions.ts b/services/platform/backend/domains/sandbox/sessions.ts index e50dd51e44..5219890e69 100644 --- a/services/platform/backend/domains/sandbox/sessions.ts +++ b/services/platform/backend/domains/sandbox/sessions.ts @@ -1,19 +1,19 @@ import type { Sql, TransactionSql } from 'postgres'; +import type { SandboxQuotaConfig } from '../../../lib/shared/schemas/governance.ts'; import { requireSessionBudgetForOwnerType, sessionBudgetForOwnerType, sessionCapFor, DEFAULT_SANDBOX_QUOTA, type SessionBudget, -} from '../../../convex/sandbox/quota_policy.ts'; +} from '../../core/sandbox/quota_policy.ts'; import { SANDBOX_MAX_SESSIONS_PER_OWNER, SANDBOX_SESSION_LIVE_STATUSES, SANDBOX_SESSION_MAX_LIFETIME_MS, -} from '../../../convex/sandbox/session_constants.ts'; -import { sessionIdForWorkflowExecution } from '../../../convex/sandbox/session_naming.ts'; -import type { SandboxQuotaConfig } from '../../../lib/shared/schemas/governance.ts'; +} from '../../core/sandbox/session_constants.ts'; +import { sessionIdForWorkflowExecution } from '../../core/sandbox/session_naming.ts'; import { toJson } from '../../db/sql.ts'; import { readGovernancePolicyForOrg } from '../../lib/org-config.ts'; import { wakeParkedAgentRuns } from '../tasks/agent-runs.ts'; diff --git a/services/platform/backend/domains/sandbox/shim.ts b/services/platform/backend/domains/sandbox/shim.ts index 99184015ef..7fe77f6184 100644 --- a/services/platform/backend/domains/sandbox/shim.ts +++ b/services/platform/backend/domains/sandbox/shim.ts @@ -1,6 +1,6 @@ import type { Sql } from 'postgres'; -import type { ShimHandlers } from '../../lib/convex-shim.ts'; +import type { ShimHandlers } from '../../lib/ctx-shim.ts'; import { resolveAgentSecretsEnv } from '../agent_secrets/service.ts'; import { chatShimHandlers } from '../chat/shim.ts'; import { listDocumentsForAgent } from '../documents/agent-list.ts'; diff --git a/services/platform/backend/domains/sandbox/user-env.ts b/services/platform/backend/domains/sandbox/user-env.ts index bf4b7fa6d3..3783aec788 100644 --- a/services/platform/backend/domains/sandbox/user-env.ts +++ b/services/platform/backend/domains/sandbox/user-env.ts @@ -4,13 +4,13 @@ import { decryptSecret, encryptSecret, type EncryptedSecret, -} from '../../../convex/lib/secret_box.ts'; +} from '../../core/lib/secret_box.ts'; import { MAX_ENV_VARS_PER_USER, SECRET_MASK, validateEnvKey, validateEnvValue, -} from '../../../convex/sandbox/user_env_constants.ts'; +} from '../../core/sandbox/user_env_constants.ts'; import { toJson } from '../../db/sql.ts'; /** diff --git a/services/platform/backend/domains/scim/routes.ts b/services/platform/backend/domains/scim/routes.ts index 9a913eac5e..642fa409f9 100644 --- a/services/platform/backend/domains/scim/routes.ts +++ b/services/platform/backend/domains/scim/routes.ts @@ -2,35 +2,35 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; +import { defineAbilityFor } from '../../../lib/permissions/ability.ts'; +import { AppError } from '../../../lib/shared/errors/app-error'; +import { isRecord } from '../../../lib/utils/type-utils.ts'; +import type { Auth } from '../../auth/auth.ts'; +import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; +import { requireSession } from '../../auth/session.ts'; import { resourceTypes, schemas, serviceProviderConfig, -} from '../../../convex/scim/discovery.ts'; +} from '../../core/scim/discovery.ts'; import { generateScimToken, hashScimToken, scimTokenPrefix, -} from '../../../convex/scim/helpers/crypto.ts'; +} from '../../core/scim/helpers/crypto.ts'; import { scimGroupResourceImpl, scimGroupsImpl, scimUserResourceImpl, scimUsersImpl, type ScimRc, -} from '../../../convex/scim/http_actions.ts'; +} from '../../core/scim/http_actions.ts'; import { SCIM_CORS_HEADERS, scimError, scimJson, -} from '../../../convex/scim/responses.ts'; -import { defineAbilityFor } from '../../../lib/permissions/ability.ts'; -import { AppError } from '../../../lib/shared/errors/app-error'; -import { isRecord } from '../../../lib/utils/type-utils.ts'; -import type { Auth } from '../../auth/auth.ts'; -import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; -import { requireSession } from '../../auth/session.ts'; -import { createCtxShim } from '../../lib/convex-shim.ts'; +} from '../../core/scim/responses.ts'; +import { createCtxShim } from '../../lib/ctx-shim.ts'; import { ssoShimHandlers } from '../sso/shim.ts'; import { disableScim, diff --git a/services/platform/backend/domains/scim/service.ts b/services/platform/backend/domains/scim/service.ts index 05fdad5ac5..7d3ba51bff 100644 --- a/services/platform/backend/domains/scim/service.ts +++ b/services/platform/backend/domains/scim/service.ts @@ -1,13 +1,13 @@ import type { Sql, TransactionSql } from 'postgres'; -import { normalizeAuthEmail } from '../../../convex/lib/auth/normalize_auth_email.ts'; +import { AppError } from '../../../lib/shared/errors/app-error'; +import { normalizeAuthEmail } from '../../core/lib/auth/normalize_auth_email.ts'; import { classifyDeprovision, classifyUserOwnership, composeDesiredMembers, planActivation, -} from '../../../convex/scim/internal_mutations.ts'; -import { AppError } from '../../../lib/shared/errors/app-error'; +} from '../../core/scim/internal_mutations.ts'; import { createAuditLog } from '../audit_logs/service.ts'; import { resolveProvisioning } from '../sso/config.ts'; diff --git a/services/platform/backend/domains/scim/shim.ts b/services/platform/backend/domains/scim/shim.ts index c321fabe14..19b1de9cd6 100644 --- a/services/platform/backend/domains/scim/shim.ts +++ b/services/platform/backend/domains/scim/shim.ts @@ -1,7 +1,7 @@ import type { Sql } from 'postgres'; import { z } from 'zod'; -import type { ShimHandlers } from '../../lib/convex-shim.ts'; +import type { ShimHandlers } from '../../lib/ctx-shim.ts'; import { deleteGroup, deprovisionUser, diff --git a/services/platform/backend/domains/skills/routes.ts b/services/platform/backend/domains/skills/routes.ts index bfd18712e1..5951d36344 100644 --- a/services/platform/backend/domains/skills/routes.ts +++ b/services/platform/backend/domains/skills/routes.ts @@ -2,19 +2,19 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; -import { - deleteSkillForViewer, - listSkillsForViewer, - readSkillAssetForViewer, - readSkillForViewer, - saveSkillForViewer, -} from '../../../convex/skills/file_actions.ts'; import { defineAbilityFor } from '../../../lib/permissions/ability.ts'; import { AppError } from '../../../lib/shared/errors/app-error'; import type { Auth } from '../../auth/auth.ts'; import { getUserTeamIds } from '../../auth/membership.ts'; import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; import { requireSession } from '../../auth/session.ts'; +import { + deleteSkillForViewer, + listSkillsForViewer, + readSkillAssetForViewer, + readSkillForViewer, + saveSkillForViewer, +} from '../../core/skills/file_actions.ts'; import { resolveOrgSlug } from '../../lib/org-config.ts'; import { uploadSkillBundlePg } from './upload.ts'; diff --git a/services/platform/backend/domains/skills/upload.ts b/services/platform/backend/domains/skills/upload.ts index 5332d7258f..60f53b55d0 100644 --- a/services/platform/backend/domains/skills/upload.ts +++ b/services/platform/backend/domains/skills/upload.ts @@ -1,28 +1,28 @@ import type { Sql } from 'postgres'; +import { AppError } from '../../../lib/shared/errors/app-error'; +import { MAX_SKILL_BUNDLE_TOTAL_BYTES } from '../../../lib/shared/schemas/skills.ts'; +import { readOrgSkill } from '../../../lib/skills/listing.ts'; +import { SkillParseError } from '../../../lib/skills/parse.ts'; +import { + canEditSkill, + type UserSkillViewer, +} from '../../../lib/skills/visibility.ts'; import { parseBlobRef, s3KeyBelongsToOrg, -} from '../../../convex/lib/storage/blob_ref.ts'; +} from '../../core/lib/storage/blob_ref.ts'; import { s3DeleteObject, s3GetObjectBytes, -} from '../../../convex/lib/storage/object_store.ts'; -import { parseSkillBundleZip } from '../../../convex/skills/bundle_zip.ts'; -import { normalizedBundleFiles } from '../../../convex/skills/file_actions.ts'; +} from '../../core/lib/storage/object_store.ts'; +import { parseSkillBundleZip } from '../../core/skills/bundle_zip.ts'; +import { normalizedBundleFiles } from '../../core/skills/file_actions.ts'; import { createOrgSkillReader, listSkillBundleFileEntries, writeSkillBundleFiles, -} from '../../../convex/skills/file_utils.ts'; -import { AppError } from '../../../lib/shared/errors/app-error'; -import { MAX_SKILL_BUNDLE_TOTAL_BYTES } from '../../../lib/shared/schemas/skills.ts'; -import { readOrgSkill } from '../../../lib/skills/listing.ts'; -import { SkillParseError } from '../../../lib/skills/parse.ts'; -import { - canEditSkill, - type UserSkillViewer, -} from '../../../lib/skills/visibility.ts'; +} from '../../core/skills/file_utils.ts'; import { resolveObjectStore } from '../../lib/object-store.ts'; /** diff --git a/services/platform/backend/domains/sso/admin.ts b/services/platform/backend/domains/sso/admin.ts index a63e7db431..019308fb29 100644 --- a/services/platform/backend/domains/sso/admin.ts +++ b/services/platform/backend/domains/sso/admin.ts @@ -1,18 +1,18 @@ import type { Sql } from 'postgres'; -import { - persistFiles, - readExisting, - removeConnectionFiles, -} from '../../../convex/enterprise_sso/config/file_store.ts'; -import { withoutGraphFileScopes } from '../../../convex/enterprise_sso/entra_id/constants.ts'; -import { getAdapter } from '../../../convex/enterprise_sso/registry.ts'; -import { fetchAndParseIdpMetadataImpl } from '../../../convex/enterprise_sso/saml/parse_metadata.ts'; -import { getPublicHttpApiUrl } from '../../../convex/lib/helpers/public_storage_url.ts'; import type { SsoConnectionFile, SsoConnectionSecrets, } from '../../../lib/shared/schemas/enterprise_sso.ts'; +import { + persistFiles, + readExisting, + removeConnectionFiles, +} from '../../core/enterprise_sso/config/file_store.ts'; +import { withoutGraphFileScopes } from '../../core/enterprise_sso/entra_id/constants.ts'; +import { getAdapter } from '../../core/enterprise_sso/registry.ts'; +import { fetchAndParseIdpMetadataImpl } from '../../core/enterprise_sso/saml/parse_metadata.ts'; +import { getPublicHttpApiUrl } from '../../core/lib/helpers/public_storage_url.ts'; import { resolveOrgSlug } from '../../lib/org-config.ts'; import { createAuditLog } from '../audit_logs/service.ts'; import { getScimStatus } from '../scim/service.ts'; diff --git a/services/platform/backend/domains/sso/config.ts b/services/platform/backend/domains/sso/config.ts index 2e97a80adb..e418ba12a0 100644 --- a/services/platform/backend/domains/sso/config.ts +++ b/services/platform/backend/domains/sso/config.ts @@ -9,9 +9,9 @@ import { validateSsoConnectionData, type SsoConnectionFile, type SsoConnectionSecrets, -} from '../../../convex/enterprise_sso/file_utils.ts'; -import { readDomainConfigFile } from '../../../convex/lib/config_store/read_domain_file.ts'; -import { readFileSafe } from '../../../convex/lib/file_io.ts'; +} from '../../core/enterprise_sso/file_utils.ts'; +import { readDomainConfigFile } from '../../core/lib/config_store/read_domain_file.ts'; +import { readFileSafe } from '../../core/lib/file_io.ts'; import { resolveOrgSlug } from '../../lib/org-config.ts'; /** diff --git a/services/platform/backend/domains/sso/routes.ts b/services/platform/backend/domains/sso/routes.ts index 5bffbd2d82..431ce5ebc0 100644 --- a/services/platform/backend/domains/sso/routes.ts +++ b/services/platform/backend/domains/sso/routes.ts @@ -1,18 +1,18 @@ import { Hono } from 'hono'; import type { Sql } from 'postgres'; -import { ssoAuthorizeHandler } from '../../../convex/enterprise_sso/login/authorize_handler.ts'; -import { ssoCallbackHandler } from '../../../convex/enterprise_sso/login/callback_handler.ts'; -import { ssoDiscoverHandler } from '../../../convex/enterprise_sso/login/discover_handler.ts'; +import { ssoAuthorizeHandler } from '../../core/enterprise_sso/login/authorize_handler.ts'; +import { ssoCallbackHandler } from '../../core/enterprise_sso/login/callback_handler.ts'; +import { ssoDiscoverHandler } from '../../core/enterprise_sso/login/discover_handler.ts'; import type { FinishLogin, FinishLoginArgs, -} from '../../../convex/enterprise_sso/login/finish_login.ts'; -import { samlAcsHandler } from '../../../convex/enterprise_sso/saml/acs_handler.ts'; -import { samlLoginHandler } from '../../../convex/enterprise_sso/saml/login_handler.ts'; -import { samlMetadataHandler } from '../../../convex/enterprise_sso/saml/metadata_handler.ts'; -import { signCookieValue } from '../../../convex/enterprise_sso/sign_cookie_value.ts'; -import { createCtxShim } from '../../lib/convex-shim.ts'; +} from '../../core/enterprise_sso/login/finish_login.ts'; +import { samlAcsHandler } from '../../core/enterprise_sso/saml/acs_handler.ts'; +import { samlLoginHandler } from '../../core/enterprise_sso/saml/login_handler.ts'; +import { samlMetadataHandler } from '../../core/enterprise_sso/saml/metadata_handler.ts'; +import { signCookieValue } from '../../core/enterprise_sso/sign_cookie_value.ts'; +import { createCtxShim } from '../../lib/ctx-shim.ts'; import { ssoShimHandlers } from './shim.ts'; /** diff --git a/services/platform/backend/domains/sso/service.ts b/services/platform/backend/domains/sso/service.ts index ce901c1070..6f34a85ba8 100644 --- a/services/platform/backend/domains/sso/service.ts +++ b/services/platform/backend/domains/sso/service.ts @@ -1,14 +1,14 @@ import { generateId } from 'better-auth'; import type { Sql } from 'postgres'; -import { mapEntraRoleToPlatformRole } from '../../../convex/enterprise_sso/entra_id/role_mapping.ts'; -import { shouldSyncMemberRole } from '../../../convex/enterprise_sso/find_or_create_sso_user.ts'; +import { sessionExpiryMs } from '../../../lib/shared/session-idle.ts'; +import { mapEntraRoleToPlatformRole } from '../../core/enterprise_sso/entra_id/role_mapping.ts'; +import { shouldSyncMemberRole } from '../../core/enterprise_sso/find_or_create_sso_user.ts'; import type { PlatformRole, SsoUserInfo, -} from '../../../convex/enterprise_sso/types.ts'; -import { normalizeAuthEmail } from '../../../convex/lib/auth/normalize_auth_email.ts'; -import { sessionExpiryMs } from '../../../lib/shared/session-idle.ts'; +} from '../../core/enterprise_sso/types.ts'; +import { normalizeAuthEmail } from '../../core/lib/auth/normalize_auth_email.ts'; import { resolveProvisioning } from './config.ts'; /** diff --git a/services/platform/backend/domains/sso/shim.ts b/services/platform/backend/domains/sso/shim.ts index fe5e8a3469..52948315be 100644 --- a/services/platform/backend/domains/sso/shim.ts +++ b/services/platform/backend/domains/sso/shim.ts @@ -4,8 +4,8 @@ import { z } from 'zod'; import { buildSamlAuthnRedirectImpl, validateSamlResponseImpl, -} from '../../../convex/enterprise_sso/saml/validate_assertion.ts'; -import type { ShimHandlers } from '../../lib/convex-shim.ts'; +} from '../../core/enterprise_sso/saml/validate_assertion.ts'; +import type { ShimHandlers } from '../../lib/ctx-shim.ts'; import { createAuditLog } from '../audit_logs/service.ts'; import { discoverByEmail, diff --git a/services/platform/backend/domains/sso/trusted-headers.ts b/services/platform/backend/domains/sso/trusted-headers.ts index 02e950ead1..331cf9884d 100644 --- a/services/platform/backend/domains/sso/trusted-headers.ts +++ b/services/platform/backend/domains/sso/trusted-headers.ts @@ -1,11 +1,11 @@ import { Hono } from 'hono'; import type { Sql } from 'postgres'; -import { resolveTeams } from '../../../convex/betterAuth/trusted_headers/resolve_team_names.ts'; -import { signCookieValue } from '../../../convex/enterprise_sso/sign_cookie_value.ts'; -import { parseTeamsHeader } from '../../../convex/trusted_headers_auth/authenticate_handler.ts'; import { sessionExpiryMs } from '../../../lib/shared/session-idle.ts'; import { sanitizeInternalRedirect } from '../../../lib/shared/utils/safe-redirect.ts'; +import { resolveTeams } from '../../core/betterAuth/trusted_headers/resolve_team_names.ts'; +import { signCookieValue } from '../../core/enterprise_sso/sign_cookie_value.ts'; +import { parseTeamsHeader } from '../../core/trusted_headers_auth/authenticate_handler.ts'; import { createAuditLog } from '../audit_logs/service.ts'; /** diff --git a/services/platform/backend/domains/tasks/agent-runs.ts b/services/platform/backend/domains/tasks/agent-runs.ts index acc0d2cdd2..ca0b0ceabe 100644 --- a/services/platform/backend/domains/tasks/agent-runs.ts +++ b/services/platform/backend/domains/tasks/agent-runs.ts @@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto'; import type { Sql, TransactionSql } from 'postgres'; -import { AUTO_RETRY_MAX_ATTEMPTS } from '../../../convex/tasks/task_auto_retry.ts'; +import { AUTO_RETRY_MAX_ATTEMPTS } from '../../core/tasks/task_auto_retry.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import { recordTaskAgentRunLedgerEntry } from './run-ledger.ts'; diff --git a/services/platform/backend/domains/tasks/agent-turn-shim.ts b/services/platform/backend/domains/tasks/agent-turn-shim.ts index 45bd05c056..4558262430 100644 --- a/services/platform/backend/domains/tasks/agent-turn-shim.ts +++ b/services/platform/backend/domains/tasks/agent-turn-shim.ts @@ -1,13 +1,13 @@ import { transactSerializable } from '@tale/shared/db/serializable'; import type { Sql } from 'postgres'; -import { readSkillBundleForViewer } from '../../../convex/skills/file_actions.ts'; -import { isAutoRetryableFailure } from '../../../convex/tasks/task_auto_retry.ts'; import { AppError } from '../../../lib/shared/errors/app-error'; import { isFilePolicyType } from '../../../lib/shared/schemas/governance'; +import { readSkillBundleForViewer } from '../../core/skills/file_actions.ts'; +import { isAutoRetryableFailure } from '../../core/tasks/task_auto_retry.ts'; import { toJson } from '../../db/sql.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; -import type { ShimHandlers, ShimScheduler } from '../../lib/convex-shim.ts'; +import type { ShimHandlers, ShimScheduler } from '../../lib/ctx-shim.ts'; import { readGovernancePolicyForOrg } from '../../lib/org-config.ts'; import { orgAdapterShimHandlers } from '../knowledge/service.ts'; import { credentialShimHandlers } from '../provider_credentials/service.ts'; diff --git a/services/platform/backend/domains/tasks/comments.ts b/services/platform/backend/domains/tasks/comments.ts index 40b53883e0..95c6b19d2f 100644 --- a/services/platform/backend/domains/tasks/comments.ts +++ b/services/platform/backend/domains/tasks/comments.ts @@ -1,8 +1,8 @@ import type { Sql, TransactionSql } from 'postgres'; -import { TASK_AUDIT_ACTIONS } from '../../../convex/tasks/audit_actions.ts'; -import type { CommentEventComment } from '../../../convex/tasks/types.ts'; import { parseTaskSubjectContract } from '../../../lib/shared/schemas/task_contract.ts'; +import { TASK_AUDIT_ACTIONS } from '../../core/tasks/audit_actions.ts'; +import type { CommentEventComment } from '../../core/tasks/types.ts'; import { toJson } from '../../db/sql.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import { emitHintInTx } from '../../realtime/outbox.ts'; diff --git a/services/platform/backend/domains/tasks/date-notifications.ts b/services/platform/backend/domains/tasks/date-notifications.ts index 500cc466c2..932427d4d2 100644 --- a/services/platform/backend/domains/tasks/date-notifications.ts +++ b/services/platform/backend/domains/tasks/date-notifications.ts @@ -1,6 +1,6 @@ import type { Sql } from 'postgres'; -import { resolveDateNotifyAudience } from '../../../convex/tasks/date_notification_recipients.ts'; +import { resolveDateNotifyAudience } from '../../core/tasks/date_notification_recipients.ts'; import { notifyUser } from '../collab/service.ts'; /** diff --git a/services/platform/backend/domains/tasks/external-ref.ts b/services/platform/backend/domains/tasks/external-ref.ts index d7dff203fe..1f40e0830c 100644 --- a/services/platform/backend/domains/tasks/external-ref.ts +++ b/services/platform/backend/domains/tasks/external-ref.ts @@ -3,11 +3,11 @@ import type { Sql, TransactionSql } from 'postgres'; import { TASK_AUDIT_ACTIONS, TASK_RESOURCE_TYPE, -} from '../../../convex/tasks/audit_actions.ts'; +} from '../../core/tasks/audit_actions.ts'; import { taskWorkflowSubjectInput, truncateImportedTitle, -} from '../../../convex/tasks/helpers.ts'; +} from '../../core/tasks/helpers.ts'; import { createAuditLog } from '../audit_logs/service.ts'; import { beginRun } from '../automations/store.ts'; import { emitEvent } from '../events/emit.ts'; diff --git a/services/platform/backend/domains/tasks/kick-plan.ts b/services/platform/backend/domains/tasks/kick-plan.ts index e55285e294..256aaa1b78 100644 --- a/services/platform/backend/domains/tasks/kick-plan.ts +++ b/services/platform/backend/domains/tasks/kick-plan.ts @@ -1,6 +1,6 @@ import type { Sql } from 'postgres'; -import { resolveTaskKickResume } from '../../../convex/tasks/task_kick_resume.ts'; +import { resolveTaskKickResume } from '../../core/tasks/task_kick_resume.ts'; /** * The kick-time resume plan over PG — the 0.5 twin of diff --git a/services/platform/backend/domains/tasks/reattach.ts b/services/platform/backend/domains/tasks/reattach.ts index 23e658e502..dfa97f3873 100644 --- a/services/platform/backend/domains/tasks/reattach.ts +++ b/services/platform/backend/domains/tasks/reattach.ts @@ -1,6 +1,6 @@ import type { Sql } from 'postgres'; -import { sessionExecStatus } from '../../../convex/node_only/sandbox/helpers/session_client.ts'; +import { sessionExecStatus } from '../../core/node_only/sandbox/helpers/session_client.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import { claimRecoveryResume, RECOVERY_STALE_MS } from '../sandbox/recovery.ts'; diff --git a/services/platform/backend/domains/tasks/reviews.ts b/services/platform/backend/domains/tasks/reviews.ts index e132a68155..939d03fef6 100644 --- a/services/platform/backend/domains/tasks/reviews.ts +++ b/services/platform/backend/domains/tasks/reviews.ts @@ -1,10 +1,10 @@ import type { Sql, TransactionSql } from 'postgres'; -import { checkProjectAccess } from '../../../convex/projects/access.ts'; import { getUserTeamIds, findOrganizationMember, } from '../../auth/membership.ts'; +import { checkProjectAccess } from '../../core/projects/access.ts'; import { toJson } from '../../db/sql.ts'; import { readGovernancePolicyForOrg } from '../../lib/org-config.ts'; import { createAuditLog } from '../audit_logs/service.ts'; diff --git a/services/platform/backend/domains/tasks/routes.ts b/services/platform/backend/domains/tasks/routes.ts index d90ee6bead..39bf7ab547 100644 --- a/services/platform/backend/domains/tasks/routes.ts +++ b/services/platform/backend/domains/tasks/routes.ts @@ -3,11 +3,11 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; -import { resolveTaskServing } from '../../../convex/tasks/task_serving.ts'; import type { Auth } from '../../auth/auth.ts'; import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; import { requireSession } from '../../auth/session.ts'; -import { createCtxShim } from '../../lib/convex-shim.ts'; +import { resolveTaskServing } from '../../core/tasks/task_serving.ts'; +import { createCtxShim } from '../../lib/ctx-shim.ts'; import { checkUserRateLimit, RateLimitExceededError, diff --git a/services/platform/backend/domains/tasks/service.ts b/services/platform/backend/domains/tasks/service.ts index 28ebb140af..0bbc546ce6 100644 --- a/services/platform/backend/domains/tasks/service.ts +++ b/services/platform/backend/domains/tasks/service.ts @@ -1,20 +1,20 @@ import type { Sql, TransactionSql } from 'postgres'; +import { + defaultTaskLabelColor, + PREDEFINED_TASK_LABELS, +} from '../../../lib/shared/task-label-colors.ts'; import { checkProjectAccess, EDITOR_ROLES, -} from '../../../convex/projects/access.ts'; -import { canClaimTask } from '../../../convex/tasks/access.ts'; +} from '../../core/projects/access.ts'; +import { canClaimTask } from '../../core/tasks/access.ts'; import { TASK_AUDIT_ACTIONS, TASK_RESOURCE_TYPE, -} from '../../../convex/tasks/audit_actions.ts'; -import { initialRank, rankBetween } from '../../../convex/tasks/rank.ts'; -import { REVIEW_POLICY_REFUSAL_CODES } from '../../../convex/tasks/review_shared.ts'; -import { - defaultTaskLabelColor, - PREDEFINED_TASK_LABELS, -} from '../../../lib/shared/task-label-colors.ts'; +} from '../../core/tasks/audit_actions.ts'; +import { initialRank, rankBetween } from '../../core/tasks/rank.ts'; +import { REVIEW_POLICY_REFUSAL_CODES } from '../../core/tasks/review_shared.ts'; import { toJson } from '../../db/sql.ts'; import { readGovernancePolicyForOrg } from '../../lib/org-config.ts'; import { emitHintInTx } from '../../realtime/outbox.ts'; diff --git a/services/platform/backend/domains/teams/routes.ts b/services/platform/backend/domains/teams/routes.ts index 3ee182fad3..c39a32a9e9 100644 --- a/services/platform/backend/domains/teams/routes.ts +++ b/services/platform/backend/domains/teams/routes.ts @@ -4,11 +4,11 @@ import { Hono } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; -import { isAdmin } from '../../../convex/lib/rls/helpers/role_helpers.ts'; import type { Auth } from '../../auth/auth.ts'; import { isAdminRole } from '../../auth/membership.ts'; import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; import { requireSession } from '../../auth/session.ts'; +import { isAdmin } from '../../core/lib/rls/helpers/role_helpers.ts'; import { emitHintInTx } from '../../realtime/outbox.ts'; /** diff --git a/services/platform/backend/domains/tts/routes.ts b/services/platform/backend/domains/tts/routes.ts index 81e382eb49..de915f4397 100644 --- a/services/platform/backend/domains/tts/routes.ts +++ b/services/platform/backend/domains/tts/routes.ts @@ -2,12 +2,12 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; -import { resolveTtsModel } from '../../../convex/lib/providers/resolve_tts_model.ts'; -import { errorCodeFromCaught } from '../../../convex/tts/error_codes.ts'; import type { Auth } from '../../auth/auth.ts'; import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; import { requireSession } from '../../auth/session.ts'; -import { createCtxShim } from '../../lib/convex-shim.ts'; +import { resolveTtsModel } from '../../core/lib/providers/resolve_tts_model.ts'; +import { errorCodeFromCaught } from '../../core/tts/error_codes.ts'; +import { createCtxShim } from '../../lib/ctx-shim.ts'; import { resolveObjectStore, s3PresignGetUrl } from '../../lib/object-store.ts'; import { resolveOrgSlug } from '../../lib/org-config.ts'; import { chatShimHandlers } from '../chat/shim.ts'; diff --git a/services/platform/backend/domains/tts/service.ts b/services/platform/backend/domains/tts/service.ts index 99601726bf..3b38c862c3 100644 --- a/services/platform/backend/domains/tts/service.ts +++ b/services/platform/backend/domains/tts/service.ts @@ -1,28 +1,10 @@ import type { Sql, TransactionSql } from 'postgres'; -import { - checkRuleAgainstUsage, - collectAllApplicableRules, - resolveEffectiveLimits, - type BudgetCheckResult, -} from '../../../convex/governance/budget_enforcement.ts'; -import { estimateTtsCostCents } from '../../../convex/governance/cost_estimation.ts'; -import { buildPeriodKey } from '../../../convex/governance/helpers.ts'; -import { checkProviderHostPolicy } from '../../../convex/lib/http/host_policy.ts'; +import { checkProviderHostPolicy } from '../../../lib/net/host-policy.ts'; import { SafeFetchError, safeFetchBinary, -} from '../../../convex/lib/http/safe_fetch.ts'; -import { resolveTtsModel } from '../../../convex/lib/providers/resolve_tts_model.ts'; -import { sanitizeError } from '../../../convex/lib/utils/sanitize_secrets.ts'; -import { AUDIO_MIME_BY_FORMAT } from '../../../convex/tts/audio_mime.ts'; -import { - errorCodeFromCaught, - parseRetryAfterMs, - TtsProviderHttpError, - ttsErrorCodeLiterals, - type TtsErrorCode, -} from '../../../convex/tts/error_codes.ts'; +} from '../../../lib/net/safe-fetch.ts'; import { MAX_TTS_CHARS_PER_MESSAGE, MAX_TTS_CHUNK_CHARS, @@ -33,9 +15,27 @@ import { } from '../../../lib/shared/constants/tts.ts'; import { TTS_SLUG } from '../../../lib/shared/constants/usage.ts'; import { getUserTeamIds } from '../../auth/membership.ts'; +import { + checkRuleAgainstUsage, + collectAllApplicableRules, + resolveEffectiveLimits, + type BudgetCheckResult, +} from '../../core/governance/budget_enforcement.ts'; +import { estimateTtsCostCents } from '../../core/governance/cost_estimation.ts'; +import { buildPeriodKey } from '../../core/governance/helpers.ts'; +import { resolveTtsModel } from '../../core/lib/providers/resolve_tts_model.ts'; +import { sanitizeError } from '../../core/lib/utils/sanitize_secrets.ts'; +import { AUDIO_MIME_BY_FORMAT } from '../../core/tts/audio_mime.ts'; +import { + errorCodeFromCaught, + parseRetryAfterMs, + TtsProviderHttpError, + ttsErrorCodeLiterals, + type TtsErrorCode, +} from '../../core/tts/error_codes.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import type { TaskPayloads } from '../../jobs/tasks.ts'; -import { createCtxShim } from '../../lib/convex-shim.ts'; +import { createCtxShim } from '../../lib/ctx-shim.ts'; import { readGovernancePolicyForOrg } from '../../lib/org-config.ts'; import { checkOrganizationRateLimit, diff --git a/services/platform/backend/domains/two_factor/service.ts b/services/platform/backend/domains/two_factor/service.ts index e0c4c4ab40..a412f762ec 100644 --- a/services/platform/backend/domains/two_factor/service.ts +++ b/services/platform/backend/domains/two_factor/service.ts @@ -1,13 +1,13 @@ import { symmetricDecrypt } from 'better-auth/crypto'; import type { Sql, TransactionSql } from 'postgres'; -import { mergeStrictestTwoFactorPolicy } from '../../../convex/governance/helpers.ts'; +import { DEFAULT_TWO_FACTOR_POLICY } from '../../../lib/shared/schemas/governance.ts'; +import { mergeStrictestTwoFactorPolicy } from '../../core/governance/helpers.ts'; import { computeLockedUntil, DEFAULT_LOGIN_POLICY, selectStrictestPolicy, -} from '../../../convex/login_attempts/helpers.ts'; -import { DEFAULT_TWO_FACTOR_POLICY } from '../../../lib/shared/schemas/governance.ts'; +} from '../../core/login_attempts/helpers.ts'; import { readGovernancePolicyForOrg } from '../../lib/org-config.ts'; import { createAuditLog } from '../audit_logs/service.ts'; diff --git a/services/platform/backend/domains/video_links/service.ts b/services/platform/backend/domains/video_links/service.ts index 245208fbdf..f3abc223f8 100644 --- a/services/platform/backend/domains/video_links/service.ts +++ b/services/platform/backend/domains/video_links/service.ts @@ -1,6 +1,5 @@ import type { Sql, TransactionSql } from 'postgres'; -import { ingestVideoLinkImpl } from '../../../convex/video_links/ingest_video_link.ts'; import { CHAT_AUDIO_MAX_DURATION_SEC } from '../../../lib/shared/file-types.ts'; import { isPlaylistUrl, @@ -8,13 +7,14 @@ import { normalizeUrlForHash, } from '../../../lib/shared/video-url.ts'; import { getUserTeamIds } from '../../auth/membership.ts'; +import { ingestVideoLinkImpl } from '../../core/video_links/ingest_video_link.ts'; import { toJson } from '../../db/sql.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import { createCtxShim, type ShimHandlers, type ShimScheduler, -} from '../../lib/convex-shim.ts'; +} from '../../lib/ctx-shim.ts'; import { createAuditLog } from '../audit_logs/service.ts'; import { claimBrowserSession, diff --git a/services/platform/backend/domains/webdav/connector-store.ts b/services/platform/backend/domains/webdav/connector-store.ts index 123943d90e..b7621e91b0 100644 --- a/services/platform/backend/domains/webdav/connector-store.ts +++ b/services/platform/backend/domains/webdav/connector-store.ts @@ -1,11 +1,11 @@ import type { Sql } from 'postgres'; -import { s3GetObjectBytes } from '../../../convex/lib/storage/object_store.ts'; import { WebdavStoreError, type WebdavStore, } from '../../../lib/connectors/natives/index.ts'; import { AppError } from '../../../lib/shared/errors/app-error'; +import { s3GetObjectBytes } from '../../core/lib/storage/object_store.ts'; import { resolveObjectStore } from '../../lib/object-store.ts'; import { resolveOrgSlug } from '../../lib/org-config.ts'; import { putOrgBlobBytes } from '../files/service.ts'; diff --git a/services/platform/backend/domains/webdav/handlers.ts b/services/platform/backend/domains/webdav/handlers.ts index 14d9f8ce96..5229f3325e 100644 --- a/services/platform/backend/domains/webdav/handlers.ts +++ b/services/platform/backend/domains/webdav/handlers.ts @@ -1,14 +1,14 @@ import type { Sql, TransactionSql } from 'postgres'; -import { - assertGenericDocumentContentWritable, - assertRecordTrashable, -} from '../../../convex/documents/access.ts'; -import { extractExtension } from '../../../convex/documents/extract_extension.ts'; -import { canonicalResourcePath } from '../../../convex/webdav/helpers.ts'; import { AppError } from '../../../lib/shared/errors/app-error'; import { resolveFileType } from '../../../lib/shared/file-types.ts'; import { isTextBasedFile } from '../../../lib/utils/text-file-types.ts'; +import { + assertGenericDocumentContentWritable, + assertRecordTrashable, +} from '../../core/documents/access.ts'; +import { extractExtension } from '../../core/documents/extract_extension.ts'; +import { canonicalResourcePath } from '../../core/webdav/helpers.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import { buildObjectKey, @@ -34,7 +34,7 @@ import { } from '../legal_holds/service.ts'; /** - * The WebDAV backing handlers — the 0.5 twins of `convex/webdav/*` (tree, + * The WebDAV backing handlers — the 0.5 twins of `backend/core/webdav/*` (tree, * locks, app passwords, org resolve) plus the blob-upload handoff, keyed by * the SAME function names the REUSED `lib/webdav` protocol layer addresses * through its ConvexHttpClient. `client-shim.ts` maps those names here, so diff --git a/services/platform/backend/domains/webdav/routes.ts b/services/platform/backend/domains/webdav/routes.ts index 64b960114d..3a38438bfb 100644 --- a/services/platform/backend/domains/webdav/routes.ts +++ b/services/platform/backend/domains/webdav/routes.ts @@ -2,11 +2,6 @@ import { Hono } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; -import { - generateAppPasswordSecret, - hmacHash, - requireHmacSecret, -} from '../../../convex/webdav/helpers.ts'; import { defineAbilityFor } from '../../../lib/permissions/ability.ts'; import { functionRefName } from '../../../lib/shared/handlers/function-refs.ts'; import { fetchAdapter } from '../../../lib/webdav/adapters/fetch.ts'; @@ -14,6 +9,11 @@ import type { WebDAVCtx } from '../../../lib/webdav/types.ts'; import type { Auth } from '../../auth/auth.ts'; import { requireOrgMember, type OrgEnv } from '../../auth/org.ts'; import { requireSession } from '../../auth/session.ts'; +import { + generateAppPasswordSecret, + hmacHash, + requireHmacSecret, +} from '../../core/webdav/helpers.ts'; import { checkOrganizationRateLimit, RateLimitExceededError, @@ -25,7 +25,7 @@ import { webdavHandlers } from './handlers.ts'; * dispatch, Basic-auth verify, PROPFIND/GET/PUT/MKCOL/DELETE/MOVE/COPY/ * LOCK/UNLOCK method handlers) served by the backend at `/dav//…`, * with its ConvexHttpClient replaced by a name-keyed shim over the PG - * handlers — the same dispatch idea as `lib/convex-shim.ts`, at the + * handlers — the same dispatch idea as `lib/ctx-shim.ts`, at the * client's `.query/.mutation/.action` surface (the protocol layer addresses * functions through the name proxy, so `functionRefName` yields the same * `path/module:export` names the handler map keys on; an unmapped name @@ -56,14 +56,14 @@ function buildWebdavCtx(sql: Sql): WebDAVCtx { action: call, }; return { - convex: shim, + backend: shim, // The /storage proxy fallback only fires when the direct-URL lane // reports the blob gone; pointing it at an unroutable origin keeps // that lane an honest 404/502 instead of a second storage door. storageBaseUrl: 'http://webdav-storage-proxy.invalid', // Upload URLs are presigned S3 PUTs (never rewritten); the Convex-POST // lane is refused up front, so there is no origin to re-home. - convexApiUrl: '', + backendApiUrl: '', }; } diff --git a/services/platform/backend/domains/websites/service.ts b/services/platform/backend/domains/websites/service.ts index a22346fcfe..af91bc82af 100644 --- a/services/platform/backend/domains/websites/service.ts +++ b/services/platform/backend/domains/websites/service.ts @@ -1,5 +1,13 @@ import type { Sql, TransactionSql } from 'postgres'; +import { + metaDescription, + normalizeListedUrl, + siteHosts, +} from '../../../lib/knowledge/crawl-parse.ts'; +import { htmlTitle } from '../../../lib/knowledge/html-to-text.ts'; +import { safeFetch } from '../../../lib/net/safe-fetch.ts'; +import { isRecord } from '../../../lib/utils/type-utils.ts'; import { deregisterDomain, fetchWebsiteInfoFromCorpus, @@ -10,41 +18,33 @@ import { registerUrlList, searchDomainContent, setScanInterval, -} from '../../../convex/knowledge/crawl.ts'; +} from '../../core/knowledge/crawl.ts'; import { scanDueWebsitesImpl, scanWebsiteImpl, -} from '../../../convex/knowledge/crawl_action.ts'; -import { getKnowledgePoolForOrg } from '../../../convex/knowledge/pool.ts'; -import { safeFetch } from '../../../convex/lib/http/safe_fetch.ts'; -import { toWebsiteDomain } from '../../../convex/websites/create_website.ts'; -import { scanIntervalToSeconds } from '../../../convex/websites/internal_actions.ts'; -import { matchesWebsiteSearch } from '../../../convex/websites/match_website_search.ts'; +} from '../../core/knowledge/crawl_action.ts'; +import { getKnowledgePoolForOrg } from '../../core/knowledge/pool.ts'; +import { toWebsiteDomain } from '../../core/websites/create_website.ts'; +import { scanIntervalToSeconds } from '../../core/websites/internal_actions.ts'; +import { matchesWebsiteSearch } from '../../core/websites/match_website_search.ts'; import { CONNECTION_FAILURES_BEFORE_PAUSE, connectionFailureCount, lastScanAttemptAt, scanPausedAt, type ScanSchedulingSite, -} from '../../../convex/websites/scan_scheduling.ts'; +} from '../../core/websites/scan_scheduling.ts'; import { isValidScanInterval, SCAN_INTERVAL_VALUES, -} from '../../../convex/websites/types.ts'; -import { - metaDescription, - normalizeListedUrl, - siteHosts, -} from '../../../lib/knowledge/crawl-parse.ts'; -import { htmlTitle } from '../../../lib/knowledge/html-to-text.ts'; -import { isRecord } from '../../../lib/utils/type-utils.ts'; +} from '../../core/websites/types.ts'; import { toJson } from '../../db/sql.ts'; import { addJobInTx } from '../../jobs/enqueue.ts'; import { createCtxShim, type ShimHandlers, type ShimScheduler, -} from '../../lib/convex-shim.ts'; +} from '../../lib/ctx-shim.ts'; import { resolveOrgSlug } from '../../lib/org-config.ts'; import { knowledgeShimHandlers } from '../knowledge/service.ts'; import { writeNotificationForOrgs } from '../notifications/service.ts'; diff --git a/services/platform/backend/integration-check.ts b/services/platform/backend/integration-check.ts index a2393310ab..461a674946 100644 --- a/services/platform/backend/integration-check.ts +++ b/services/platform/backend/integration-check.ts @@ -34,10 +34,10 @@ import type { PgBoss } from 'pg-boss'; import type { Sql } from 'postgres'; import { z } from 'zod'; -import { computeAuditHash } from '../convex/lib/helpers/audit_hash.ts'; import { objectStorageConnectionFileSchema } from '../lib/shared/schemas/object_storage.ts'; import { createApp } from './app.ts'; import { createAuth, type Auth } from './auth/auth.ts'; +import { computeAuditHash } from './core/lib/helpers/audit_hash.ts'; import { runBootMigrations } from './db/migrate.ts'; import { createSql } from './db/sql.ts'; import { rowToHashInput } from './domains/audit_logs/hash-input.ts'; @@ -1820,7 +1820,7 @@ async function checkFiles( // bucket and re-run the seeder — the 'present' path re-ensures the bucket // when the default still points at the bundled store. const { buildS3ObjectStore } = - await import('../convex/lib/storage/object_store.ts'); + await import('./core/lib/storage/object_store.ts'); const probeS3 = buildS3ObjectStore( { region: 'us-east-1', endpoint, forcePathStyle: true, bucket }, { accessKeyId, secretAccessKey }, @@ -2136,7 +2136,7 @@ async function checkDocuments( : undefined; // The read fallback row must be reachable through the CHAT map too — it // was registered only on the sandbox door once, which made every chat - // `rag_fetch` inline read die as "[convex-shim] un-shimmed". + // `rag_fetch` inline read die as "[ctx-shim] un-shimmed". const readDoor = chatShimHandlers(sql)['documents/internal_queries:findDocumentByFileId']; const notesRef = userNamed.success @@ -3835,8 +3835,7 @@ async function checkKnowledge( `; return rows[0]?.status === 'completed'; }, 60_000); - const { getKnowledgePoolForOrg } = - await import('../convex/knowledge/pool.ts'); + const { getKnowledgePoolForOrg } = await import('./core/knowledge/pool.ts'); const corpusPool = await getKnowledgePoolForOrg(orgSlug); const corpusRows = await corpusPool< { status: string; total: number; stored: string }[] @@ -6471,7 +6470,7 @@ async function checkSsoLogin( const { createServer } = await import('node:http'); const { createHash } = await import('node:crypto'); const { serializeSsoConnectionYaml, resolveSsoDir } = - await import('../convex/enterprise_sso/file_utils.ts'); + await import('./core/enterprise_sso/file_utils.ts'); // --- fake OIDC IdP ------------------------------------------------------- let seenChallenge = ''; @@ -6771,7 +6770,7 @@ async function checkSamlLogin( const { createHash, createSign, generateKeyPairSync } = await import('node:crypto'); const { serializeSsoConnectionYaml, resolveSsoDir } = - await import('../convex/enterprise_sso/file_utils.ts'); + await import('./core/enterprise_sso/file_utils.ts'); const ssoDir = resolveSsoDir(orgSlug); const connectionPath = path.join(ssoDir, 'connection.yml'); @@ -7062,7 +7061,7 @@ async function checkEntraLogin( orgSlug: string, ): Promise { const { serializeSsoConnectionYaml, resolveSsoDir } = - await import('../convex/enterprise_sso/file_utils.ts'); + await import('./core/enterprise_sso/file_utils.ts'); const tenantId = '8f1e2b3c-4d5a-6789-abcd-ef0123456789'; const appRoleId = 'b31e4c77-11aa-4c8d-9f3e-2b6d5a7c9e01'; const ssoDir = resolveSsoDir(orgSlug); @@ -7926,9 +7925,9 @@ async function checkSandboxBlobDoor( const { resolveOrgSlug } = await import('./lib/org-config.ts'); const { resolveObjectStore, s3PutObject, buildObjectKey } = await import('./lib/object-store.ts'); - const { encodeS3Ref } = await import('../convex/lib/storage/blob_ref.ts'); + const { encodeS3Ref } = await import('./core/lib/storage/blob_ref.ts'); const { signStageToken } = - await import('../convex/lib/storage/sandbox_stage_token.ts'); + await import('./core/lib/storage/sandbox_stage_token.ts'); const orgSlug = (await resolveOrgSlug(sql, ctx.orgId)) ?? ''; const store = await resolveObjectStore(orgSlug); const key = buildObjectKey(store, orgSlug); @@ -9788,7 +9787,7 @@ async function checkRecoverySweeps( SELECT "slug" FROM "organization" WHERE "id" = ${orgId} LIMIT 1 `; const { getKnowledgePoolForOrg, PRIVATE_KNOWLEDGE_SCHEMA } = - await import('../convex/knowledge/pool.ts'); + await import('./core/knowledge/pool.ts'); let corpusSeeded = false; try { const pool = await getKnowledgePoolForOrg(orgSlugRow[0]?.slug ?? ''); @@ -10255,7 +10254,7 @@ async function checkConnectorOauth( // proved the HTTP shell. const happyState = 'itest-oauth-state-value'; const { hashStateToken } = - await import('../convex/http_connectors/oauth_state.ts'); + await import('./core/http_connectors/oauth_state.ts'); await oauth.createPendingAuthorization(sql, { stateHash: await hashStateToken(happyState), organizationId: orgId, @@ -13391,7 +13390,7 @@ async function checkOauthAppSsoReuse( ): Promise { const { cookie, orgId } = ctx; const { serializeSsoConnectionYaml, resolveSsoDir } = - await import('../convex/enterprise_sso/file_utils.ts'); + await import('./core/enterprise_sso/file_utils.ts'); const savedSiteUrl = process.env.SITE_URL; process.env.SITE_URL = base; @@ -13577,7 +13576,7 @@ async function checkCloudImport( storeCloudAuthorization, } = await import('./domains/cloud_import/service.ts'); const { hashStateToken } = - await import('../convex/http_connectors/oauth_state.ts'); + await import('./core/http_connectors/oauth_state.ts'); // Start: a signed-in member gets a 302 to the vendor with PKCE + state. const start = await fetch( @@ -14744,7 +14743,7 @@ async function checkWebsitesCrawl( ): Promise { const { cookie, orgId } = ctx; const websites = await import('./domains/websites/service.ts'); - const scheduling = await import('../convex/websites/scan_scheduling.ts'); + const scheduling = await import('./core/websites/scan_scheduling.ts'); const DOMAIN = 'itest-crawl.example'; const site = new Map< @@ -14801,7 +14800,7 @@ async function checkWebsitesCrawl( preconnect: (): void => {}, }); - const corpus = await import('../convex/knowledge/pool.ts'); + const corpus = await import('./core/knowledge/pool.ts'); const pool = corpus.getKnowledgePool(); // Park the org's embedding config (an earlier check pointed it at a now- @@ -15911,8 +15910,7 @@ async function checkBrowserSessions( ): Promise { const { cookie, orgId, userId } = ctx; const browser = await import('./domains/browser_sessions/service.ts'); - const { decryptString } = - await import('../convex/lib/crypto/decrypt_string.ts'); + const { decryptString } = await import('./core/lib/crypto/decrypt_string.ts'); const savedAdmins = process.env.TALE_DEPLOYMENT_CONFIG_ADMINS; const emailRows = await sql<{ email: string }[]>` SELECT "email" FROM "user" WHERE "id" = ${userId} LIMIT 1 @@ -22218,7 +22216,7 @@ async function checkDataResidency( const byoBucket = 'itest-byo'; // Create the BYO bucket directly (MinIO: signed PUT on the bucket URL). const { buildS3ObjectStore } = - await import('../convex/lib/storage/object_store.ts'); + await import('./core/lib/storage/object_store.ts'); const byoStore = buildS3ObjectStore( { region: 'us-east-1', @@ -22377,9 +22375,8 @@ async function checkDataResidency( // A migrated object is REALLY in the BYO bucket now. let landed = false; if (realStatus !== null && realStatus.sample.length > 0) { - const { parseBlobRef } = await import('../convex/lib/storage/blob_ref.ts'); - const { s3HeadObject } = - await import('../convex/lib/storage/object_store.ts'); + const { parseBlobRef } = await import('./core/lib/storage/blob_ref.ts'); + const { s3HeadObject } = await import('./core/lib/storage/object_store.ts'); const sampleRef = realStatus.sample[0]?.ref ?? ''; try { const parsed = parseBlobRef(sampleRef); @@ -23465,7 +23462,7 @@ async function checkMetricsSurface( // ---- the run dialog's execution log --------------------------------- const { sessionIdForWorkflowExecution } = - await import('../convex/sandbox/session_naming.ts'); + await import('./core/sandbox/session_naming.ts'); const logRun = await sql<{ id: string }[]>` INSERT INTO app.automation_runs ( org_id, name, version, status, mode, started_by, started_at_ms diff --git a/services/platform/backend/jobs/task-list.ts b/services/platform/backend/jobs/task-list.ts index 741e40f6d0..788a4d5aad 100644 --- a/services/platform/backend/jobs/task-list.ts +++ b/services/platform/backend/jobs/task-list.ts @@ -5,16 +5,16 @@ import { driveWorkflowAgentTurnImpl, resumeWorkflowAgentTurnWithAnswerImpl, startWorkflowAgentTurnImpl, -} from '../../convex/automations/agent_host.ts'; -import { stepRunImpl } from '../../convex/automations/stepper.ts'; -import { generateThreadTitleImpl } from '../../convex/chat/generate_title.ts'; -import { removeOrgSubtree } from '../../convex/organizations/scaffold.ts'; +} from '../core/automations/agent_host.ts'; +import { stepRunImpl } from '../core/automations/stepper.ts'; +import { generateThreadTitleImpl } from '../core/chat/generate_title.ts'; +import { removeOrgSubtree } from '../core/organizations/scaffold.ts'; import { driveTaskAgentTurnImpl, startTaskAgentTurnImpl, steerTaskAgentTurnImpl, -} from '../../convex/tasks/agent_run_host.ts'; -import { resolveAutoRetryBudget } from '../../convex/tasks/task_auto_retry.ts'; +} from '../core/tasks/agent_run_host.ts'; +import { resolveAutoRetryBudget } from '../core/tasks/task_auto_retry.ts'; import { automationShimHandlers, automationShimScheduler, @@ -59,7 +59,7 @@ import { runWebsitesScan, runWebsitesScanDue, } from '../domains/websites/service.ts'; -import { createCtxShim } from '../lib/convex-shim.ts'; +import { createCtxShim } from '../lib/ctx-shim.ts'; /** One task handler; `payload` is a job row — external input, re-validate. */ export type TaskHandler = (payload: unknown) => Promise; diff --git a/services/platform/backend/lib/convex-shim.ts b/services/platform/backend/lib/ctx-shim.ts similarity index 92% rename from services/platform/backend/lib/convex-shim.ts rename to services/platform/backend/lib/ctx-shim.ts index e59cdac5c4..842344bd78 100644 --- a/services/platform/backend/lib/convex-shim.ts +++ b/services/platform/backend/lib/ctx-shim.ts @@ -32,7 +32,7 @@ function dispatcher(kind: string, handlers: ShimHandlers) { const name = shimFunctionName(ref); const handler = handlers[name]; if (!handler) { - throw new Error(`[convex-shim] un-shimmed ${kind} call: ${name}`); + throw new Error(`[ctx-shim] un-shimmed ${kind} call: ${name}`); } return handler(args); }; @@ -40,7 +40,7 @@ function dispatcher(kind: string, handlers: ShimHandlers) { function refuse(facility: string): () => never { return () => { - throw new Error(`[convex-shim] ctx.${facility} is not available in 0.5`); + throw new Error(`[ctx-shim] ctx.${facility} is not available in 0.5`); }; } @@ -82,7 +82,7 @@ export function createCtxShim( runAfter: async (delayMs, ref, args) => { if (scheduler === undefined) { throw new Error( - '[convex-shim] ctx.scheduler.runAfter is not available in 0.5 (no scheduler seam registered)', + '[ctx-shim] ctx.scheduler.runAfter is not available in 0.5 (no scheduler seam registered)', ); } await scheduler(shimFunctionName(ref), delayMs, args); diff --git a/services/platform/backend/lib/governance-policies.ts b/services/platform/backend/lib/governance-policies.ts index 81efa44478..e4a038b768 100644 --- a/services/platform/backend/lib/governance-policies.ts +++ b/services/platform/backend/lib/governance-policies.ts @@ -3,13 +3,13 @@ import path from 'node:path'; import type { Sql, TransactionSql } from 'postgres'; -import { resolveGovernanceDir } from '../../convex/governance/file_utils.ts'; import { DEFAULT_PASSWORD_POLICY, mergeStrictestPasswordPolicy, policyTypeToFileBase, type PasswordPolicyConfig, } from '../../lib/shared/schemas/governance.ts'; +import { resolveGovernanceDir } from '../core/governance/file_utils.ts'; import { readGovernancePolicyForOrg, resolveOrgSlug } from './org-config.ts'; /** diff --git a/services/platform/backend/lib/governance-policy-write.ts b/services/platform/backend/lib/governance-policy-write.ts index 5f805e8362..cefdf32920 100644 --- a/services/platform/backend/lib/governance-policy-write.ts +++ b/services/platform/backend/lib/governance-policy-write.ts @@ -1,21 +1,21 @@ import { mkdir } from 'node:fs/promises'; import path from 'node:path'; +import type { FilePolicyType } from '../../lib/shared/schemas/governance.ts'; import { MAX_HISTORY_ENTRIES, resolveHistoryDir, resolvePolicyFilePath, resolvePolicyYamlFilePath, serializePolicyYaml, -} from '../../convex/governance/file_utils.ts'; +} from '../core/governance/file_utils.ts'; import { atomicWrite, generateHistoryTimestamp, pruneHistory, readFileSafe, removeFileSafe, -} from '../../convex/lib/file_io.ts'; -import type { FilePolicyType } from '../../lib/shared/schemas/governance.ts'; +} from '../core/lib/file_io.ts'; import { clearOrgConfigCaches } from './org-config.ts'; /** diff --git a/services/platform/backend/lib/object-store.ts b/services/platform/backend/lib/object-store.ts index cb9f01b8f2..eee4787697 100644 --- a/services/platform/backend/lib/object-store.ts +++ b/services/platform/backend/lib/object-store.ts @@ -7,8 +7,8 @@ import { s3PresignPutUrl, s3PutObject, type S3ObjectStore, -} from '../../convex/lib/storage/object_store.ts'; -import { readOrgObjectStorageConnection } from '../../convex/object_storage/file_utils.ts'; +} from '../core/lib/storage/object_store.ts'; +import { readOrgObjectStorageConnection } from '../core/object_storage/file_utils.ts'; /** * 0.5 object-store resolution — S3-compatible storage is THE blob backend diff --git a/services/platform/backend/lib/org-config.ts b/services/platform/backend/lib/org-config.ts index 8fc2a97dd6e80379fa732517da6be4be808d2ce8..33a73fb1c40c527800539e6970ffbe48d3f738f7 100644 GIT binary patch delta 33 kcmX@Cx>I$6-Na|b6U{VPlJkpFCuX=p=xrLCBN?*<0qbH6{r~^~ delta 44 scmdm~dRTRW-Nc=WoO*it$@zI@sTC9Nxgt1|)fqJ=npJQ1XUq}=0CH~+r2qf` diff --git a/services/platform/backend/node-loader.mjs b/services/platform/backend/node-loader.mjs index 259a63480b..3d8219cedc 100644 --- a/services/platform/backend/node-loader.mjs +++ b/services/platform/backend/node-loader.mjs @@ -2,14 +2,12 @@ * Node ESM resolve hook for the 0.5 backend runtime. * * The backend runs as plain `node main.ts` (type-stripped, no bundler), so - * Node's ESM resolver demands fully-specified relative imports. The 0.4 - * platform tree (`services/platform/lib/**`, reusable pure modules under - * `services/platform/convex/**`) was written for bundler resolution and uses - * extensionless relative imports throughout. Rather than fork-copying every - * pure helper the port reuses (and silently drifting from 0.4 while both - * trees are live), this hook teaches Node the bundler convention: when a - * relative/absolute specifier fails to resolve, retry `.ts`, then - * `/index.ts`. + * Node's ESM resolver demands fully-specified relative imports. Much of the + * tree it runs (`services/platform/lib/**` and the ported domain logic under + * `services/platform/backend/core/**`) was written for bundler resolution + * and uses extensionless relative imports throughout. This hook teaches Node + * the bundler convention: when a relative/absolute specifier fails to + * resolve, retry `.ts`, then `/index.ts`. * * Registered via `node --import ./backend/node-loader.mjs …` in the backend * scripts and the container entrypoint. Package specifiers are never diff --git a/services/platform/backend/realtime/oracle-routes.ts b/services/platform/backend/realtime/oracle-routes.ts index 31c8b65069..9d9af18ee1 100644 --- a/services/platform/backend/realtime/oracle-routes.ts +++ b/services/platform/backend/realtime/oracle-routes.ts @@ -19,9 +19,9 @@ import { Hono } from 'hono'; import type { Sql } from 'postgres'; -import { getClientIp } from '../../convex/lib/utils/client_ip.ts'; import { loadTrustedProxies, type Auth } from '../auth/auth.ts'; import { getUserOrganizations } from '../auth/membership.ts'; +import { getClientIp } from '../core/lib/utils/client_ip.ts'; import { loadProjectSharedThread } from '../domains/chat/threads.ts'; import { listLiveSessionsForOwner } from '../domains/sandbox/sessions.ts'; import { checkIpRateLimit, RateLimitExceededError } from '../lib/rate-limit.ts'; diff --git a/services/platform/backend/rest/shared.ts b/services/platform/backend/rest/shared.ts index 07d3e38b8c..27b94d638e 100644 --- a/services/platform/backend/rest/shared.ts +++ b/services/platform/backend/rest/shared.ts @@ -1,8 +1,8 @@ import type { Context } from 'hono'; import type { Sql } from 'postgres'; -import { EDITOR_ROLES } from '../../convex/projects/access.ts'; import { defineAbilityFor } from '../../lib/permissions/ability.ts'; +import { EDITOR_ROLES } from '../core/projects/access.ts'; import { resolveUserOrganization } from '../domains/organizations/service.ts'; import { getProjectAuthContext } from '../domains/projects/service.ts'; import { RateLimitExceededError, checkIpRateLimit } from '../lib/rate-limit.ts'; diff --git a/services/platform/backend/rest/v1-core.ts b/services/platform/backend/rest/v1-core.ts index 6d9a5d7176..3107fc52c0 100644 --- a/services/platform/backend/rest/v1-core.ts +++ b/services/platform/backend/rest/v1-core.ts @@ -2,22 +2,22 @@ import { Hono, type Context } from 'hono'; import type { Sql } from 'postgres'; import { z } from 'zod'; +import { defineAbilityFor } from '../../lib/permissions/ability.ts'; +import { AppError } from '../../lib/shared/errors/app-error'; +import { dataSourceSchema } from '../../lib/shared/schemas/common.ts'; +import { getUserTeamIds } from '../auth/membership.ts'; import { deleteAgentForCaller, listAgentsForCaller, readAgentForCaller, saveAgentForCaller, -} from '../../convex/agents/file_actions.ts'; +} from '../core/agents/file_actions.ts'; import { deleteSkillForViewer, listSkillsForViewer, readSkillForViewer, saveSkillForViewer, -} from '../../convex/skills/file_actions.ts'; -import { defineAbilityFor } from '../../lib/permissions/ability.ts'; -import { AppError } from '../../lib/shared/errors/app-error'; -import { dataSourceSchema } from '../../lib/shared/schemas/common.ts'; -import { getUserTeamIds } from '../auth/membership.ts'; +} from '../core/skills/file_actions.ts'; import { bulkCreateContacts, createContact, diff --git a/services/platform/backend/rest/v1-mcp.ts b/services/platform/backend/rest/v1-mcp.ts index d08b817d36..be5e32a594 100644 --- a/services/platform/backend/rest/v1-mcp.ts +++ b/services/platform/backend/rest/v1-mcp.ts @@ -1,19 +1,19 @@ import { Hono } from 'hono'; import type { Sql } from 'postgres'; -import { - handleMcpRequest, - mcpGetNotAllowed, -} from '../../convex/automations_builder/mcp_http.ts'; -import { loadConnectorDefinitions } from '../../convex/connector_credentials/connector_catalog.ts'; import { installConnectorCatalog } from '../../lib/connectors/dispatcher.ts'; import { registerConnector } from '../../lib/connectors/registry.ts'; import { dispatch } from '../../lib/engine/api/dispatch.ts'; import { hasCodeRunner, setCodeRunner } from '../../lib/engine/core/runner.ts'; import { nodeVmRunner } from '../../lib/engine/runners/node-vm.ts'; +import { + handleMcpRequest, + mcpGetNotAllowed, +} from '../core/automations_builder/mcp_http.ts'; +import { loadConnectorDefinitions } from '../core/connector_credentials/connector_catalog.ts'; import { pgAutomationStore } from '../domains/automations/dispatch-store.ts'; import { dispatchCapabilityAs } from '../domains/chat/capabilities.ts'; -import { createCtxShim, type ShimHandlers } from '../lib/convex-shim.ts'; +import { createCtxShim, type ShimHandlers } from '../lib/ctx-shim.ts'; import type { RestEnv } from './shared.ts'; /** diff --git a/services/platform/backend/rest/v1-websites.ts b/services/platform/backend/rest/v1-websites.ts index 1b83d64d0d..bcc792b4d6 100644 --- a/services/platform/backend/rest/v1-websites.ts +++ b/services/platform/backend/rest/v1-websites.ts @@ -4,7 +4,7 @@ import type { Sql } from 'postgres'; import { isValidScanInterval, SCAN_INTERVAL_VALUES, -} from '../../convex/websites/types.ts'; +} from '../core/websites/types.ts'; import { createWebsiteRow, deregisterAndDeleteWebsite, diff --git a/services/platform/convex/documents/extract_extension.ts b/services/platform/convex/documents/extract_extension.ts deleted file mode 100644 index 3d36374771..0000000000 --- a/services/platform/convex/documents/extract_extension.ts +++ /dev/null @@ -1 +0,0 @@ -export { extractExtension } from '../../lib/shared/file-types'; diff --git a/services/platform/convex/webdav/README.md b/services/platform/convex/webdav/README.md deleted file mode 100644 index edc6c20ef1..0000000000 --- a/services/platform/convex/webdav/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# WebDAV Convex module — trust boundary - -The functions in this directory are split into two visibility classes: - -## Public (UI-callable, full Better Auth check) - -- `app_password_mutations.createAppPassword` — generates a random secret + HMAC hash, inserts a row, returns plaintext **once** -- `app_password_mutations.revokeAppPassword` — soft-revoke a row owned by the caller -- `app_password_queries.listAppPasswords` — list rows owned by the caller (metadata only — no hash, no plaintext) - -These run the full `authComponent.getAuthUser` check and only operate on rows where `userId === authUser.userId`. - -## Internal (admin-key, called from the platform Hono server) - -Everything else (`*_internal`, `findCandidatesByPrefix`, `recordAppPasswordUse`, all `lock_*` and `tree_*` functions). - -The platform Hono server (`services/platform/server.ts`) opens a `ConvexHttpClient` configured with `ADMIN_KEY` at startup and calls these via `internal.webdav.*` after performing its own per-request HTTP Basic auth check using `findCandidatesByPrefix` + HMAC comparison. - -**Why this split**: WebDAV uses HTTP Basic — the credentials are not a Better Auth session cookie that Convex can validate via `authComponent.getAuthUser`. The platform server holds the trust: it parses the `Authorization: Basic` header, verifies the app-password, then asserts `organizationId` + `userId` to Convex. Convex internal functions trust those assertions — they do **not** re-check user identity. This is the same pattern `services/platform/convex/http.ts:362-366` uses for `/api/sse/auth` (the platform Hono server reads the session cookie, then queries Convex with the user's id). - -**Hub-only visibility**: WebDAV applies no team ACLs (the Hono-trust split above asserts only `organizationId` + `userId`), but the tree functions do enforce document scope: project-scoped documents (`documents.projectId` set) are **not** WebDAV resources (#2545). `webdav/visibility.ts` (`isWebdavVisibleDocument`, built on `documents/access.ts`'s `isProjectScopedDocument`) gates every listing, leaf resolution, and name-collision lookup — project files never list, resolve as not-found for every caller (mirroring the REST 404s), and a PUT whose name collides with one creates an independent hub document. Project members reach those files through the project surfaces instead. - -Project-scoped **folders** (`folders.projectId` set) are excluded the same way, but at the index rather than a predicate: every folder listing, path segment, and name-collision lookup in `tree_queries.ts` / `tree_mutations.ts` (and the shared `folders/find_folder_by_path.ts`) queries `by_org_project_parent_name` pinned to `projectId=undefined`, so a project folder never lists, never resolves, and never counts as a MKCOL/PUT collision. Recursive descendant walks (cascade delete, copy, move fixup, hold guard) stay on `by_org_parent_name` — they descend from an already-hub-authorized root, and a folder's children share its scope by invariant. - -**HMAC secret**: `WEBDAV_APP_PASSWORD_HMAC_KEY` (hex-encoded 32-byte random). Derived deterministically from `INSTANCE_SECRET` by `docker-entrypoint.sh` (prod) and `server.ts` (dev) — operators do not set this manually. To rotate the HMAC independently of `INSTANCE_SECRET`, set `WEBDAV_APP_PASSWORD_HMAC_KEY` explicitly in `.env`; an explicit value always overrides the derived one. **Rotating it invalidates every existing app-password.** diff --git a/services/platform/docker-entrypoint.sh b/services/platform/docker-entrypoint.sh index b836d79e1e..faa3260d28 100644 --- a/services/platform/docker-entrypoint.sh +++ b/services/platform/docker-entrypoint.sh @@ -32,10 +32,9 @@ log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR $*" >&2; } log_section() { echo; echo "════════════════════════════════════"; echo " $*"; echo "════════════════════════════════════"; } # ---------------------------------------------------------------------------- -# Privilege handling: platform no longer owns /app/data. The only thing the -# re-exec dance still does is make sure we run as the app user so that Bun -# picks up the right HOME etc. Volume ownership is now the convex container's -# problem. +# Privilege handling: the re-exec dance makes sure we run as the app user so +# that Bun picks up the right HOME etc. (the entrypoint re-asserts config-volume +# ownership earlier, while still root). # ---------------------------------------------------------------------------- # ============================================================================ # SSRF egress firewall (defense-in-depth) — installed while still root @@ -226,13 +225,13 @@ ensure_instance_secret # Layout detection (prod runner vs dev image) # ---------------------------------------------------------------------------- # The production runner flattens the platform into /app: server.ts at -# /app/server.ts, functions at /app/convex, cwd /app. The dev image (Dockerfile -# `dev` stage) is the unpruned `builder` and keeps the monorepo layout: -# server.ts + vite config + functions live under /app/services/platform. One -# entrypoint serves both, so resolve the two roots from a marker that ONLY the -# flat layout has (/app/server.ts) and cd into the platform dir. Bind-mounting -# the convex tree over /app/services/platform/convex (compose.dev.yml) does NOT -# create /app/server.ts, so the detection stays correct under that mount. +# /app/server.ts, backend sources at /app/backend, cwd /app. The dev image +# (Dockerfile `dev` stage) is the unpruned `builder` and keeps the monorepo +# layout: server.ts + vite config + backend live under /app/services/platform. +# One entrypoint serves both, so resolve the two roots from a marker that ONLY +# the flat layout has (/app/server.ts) and cd into the platform dir. +# Bind-mounting sources over /app/services/platform (compose.dev.yml) does NOT +# create /app/server.ts, so the detection stays correct under those mounts. if [ -f /app/server.ts ]; then PLATFORM_DIR=/app else diff --git a/services/platform/lib/chat/context.test.ts b/services/platform/lib/chat/context.test.ts index f2a954fbc5..429841bd13 100644 --- a/services/platform/lib/chat/context.test.ts +++ b/services/platform/lib/chat/context.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { UNTRUSTED_CONTENT_SYSTEM_PROMPT } from '../../convex/lib/untrusted_content'; import { assembleContext, CONTEXT_BLOCK_ORDER, @@ -10,6 +9,7 @@ import { } from './context'; import type { ChatMessage } from './types'; import { estimateMessageTokens, estimateTokens } from './types'; +import { UNTRUSTED_CONTENT_SYSTEM_PROMPT } from './untrusted-content'; /** * The context contract is an ORDER, so these tests assert the order itself — diff --git a/services/platform/lib/chat/context.ts b/services/platform/lib/chat/context.ts index ad359db7e9..38093dd2ba 100644 --- a/services/platform/lib/chat/context.ts +++ b/services/platform/lib/chat/context.ts @@ -37,7 +37,6 @@ * Layer A: pure, no `node:*`, no Convex, no model call. */ -import { UNTRUSTED_CONTENT_SYSTEM_PROMPT } from '../../convex/lib/untrusted_content'; import { boundJson } from '../shared/utils/bound-json'; import { narrowBcp47 } from '../shared/utils/narrow-bcp47'; import { pickField } from '../shared/utils/pick-field'; @@ -47,6 +46,7 @@ import { type ChatMessage, type MessagePart, } from './types'; +import { UNTRUSTED_CONTENT_SYSTEM_PROMPT } from './untrusted-content'; /** The canonical block order. The assembler emits a subsequence of this list * — never a reordering, never an extra. */ diff --git a/services/platform/convex/lib/untrusted_content.test.ts b/services/platform/lib/chat/untrusted-content.test.ts similarity index 98% rename from services/platform/convex/lib/untrusted_content.test.ts rename to services/platform/lib/chat/untrusted-content.test.ts index d79a201dae..4101f1f613 100644 --- a/services/platform/convex/lib/untrusted_content.test.ts +++ b/services/platform/lib/chat/untrusted-content.test.ts @@ -4,7 +4,7 @@ import { containsSuspiciousInjection, escapeForXmlTag, wrapUntrusted, -} from './untrusted_content'; +} from './untrusted-content'; describe('escapeForXmlTag', () => { it('neutralizes the closing tag literal so wrappers cannot be broken', () => { diff --git a/services/platform/convex/lib/untrusted_content.ts b/services/platform/lib/chat/untrusted-content.ts similarity index 98% rename from services/platform/convex/lib/untrusted_content.ts rename to services/platform/lib/chat/untrusted-content.ts index f0f43fff2d..9f4eeacb5a 100644 --- a/services/platform/convex/lib/untrusted_content.ts +++ b/services/platform/lib/chat/untrusted-content.ts @@ -53,7 +53,7 @@ export function escapeForXmlTag(value: string, tagName: string): string { // optimistic-render path (use-send-message.ts) can call it without // crossing the convex namespace. Re-export to keep existing convex // importers (`../untrusted_content`) unchanged. -export { sanitizeUntrustedField } from '../../lib/shared/sanitize-untrusted-field'; +export { sanitizeUntrustedField } from '../shared/sanitize-untrusted-field'; export function wrapUntrusted( content: string, diff --git a/services/platform/lib/connectors/live-host.ts b/services/platform/lib/connectors/live-host.ts index 248a5e39fd..aad4c286b7 100644 --- a/services/platform/lib/connectors/live-host.ts +++ b/services/platform/lib/connectors/live-host.ts @@ -37,18 +37,14 @@ * refusal or a request that never produced a response throws. */ -import { checkProviderHostPolicy } from '../../convex/lib/http/host_policy'; -import { - safeFetch, - safeFetchBinary, - SafeFetchError, -} from '../../convex/lib/http/safe_fetch'; import type { ConnectorContext, ConnectorHostCapabilities, ConnectorHttpRequest, ConnectorHttpResponse, } from '../engine/core/slots'; +import { checkProviderHostPolicy } from '../net/host-policy'; +import { safeFetch, safeFetchBinary, SafeFetchError } from '../net/safe-fetch'; import type { Connector } from '../shared/schemas/connectors'; import { ConnectorError } from './errors'; diff --git a/services/platform/lib/harnesses/exec-builder.test.ts b/services/platform/lib/harnesses/exec-builder.test.ts index e359fa9285..2b57b8b537 100644 --- a/services/platform/lib/harnesses/exec-builder.test.ts +++ b/services/platform/lib/harnesses/exec-builder.test.ts @@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { loadHarnesses } from '../../convex/lib/providers/load_system_config'; +import { loadHarnesses } from '../../backend/core/lib/providers/load_system_config'; import type { HarnessDefinition } from '../shared/schemas/providers'; import { buildHarnessExec, isClaudeModelRef } from './exec-builder'; import { GOLDEN_BYO_ENV, GOLDEN_GATEWAY, goldenBattery } from './test-helpers'; diff --git a/services/platform/lib/harnesses/golden-exec.test.ts b/services/platform/lib/harnesses/golden-exec.test.ts index 8b81e0a357..51298d1ef6 100644 --- a/services/platform/lib/harnesses/golden-exec.test.ts +++ b/services/platform/lib/harnesses/golden-exec.test.ts @@ -9,7 +9,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { loadHarnesses } from '../../convex/lib/providers/load_system_config'; +import { loadHarnesses } from '../../backend/core/lib/providers/load_system_config'; import { composeHarnessGlue } from './registry'; import { goldenBattery, diff --git a/services/platform/lib/harnesses/registry.test.ts b/services/platform/lib/harnesses/registry.test.ts index 666623696b..8fc6574ccc 100644 --- a/services/platform/lib/harnesses/registry.test.ts +++ b/services/platform/lib/harnesses/registry.test.ts @@ -15,7 +15,7 @@ import { describe, expect, it } from 'vitest'; -import { loadHarnesses } from '../../convex/lib/providers/load_system_config'; +import { loadHarnesses } from '../../backend/core/lib/providers/load_system_config'; import { harnessDefinitionSchema, type HarnessDefinition, diff --git a/services/platform/convex/lib/http/host_policy.ts b/services/platform/lib/net/host-policy.ts similarity index 96% rename from services/platform/convex/lib/http/host_policy.ts rename to services/platform/lib/net/host-policy.ts index 7cfac4747b..24f37031a6 100644 --- a/services/platform/convex/lib/http/host_policy.ts +++ b/services/platform/lib/net/host-policy.ts @@ -18,8 +18,8 @@ * with a `lookup` callback. */ -import { AppError } from '../../../lib/shared/errors/app-error'; -import { isPrivateIp } from './safe_fetch'; +import { AppError } from '../shared/errors/app-error'; +import { isPrivateIp } from './safe-fetch'; /** * Cloud metadata endpoints, including public-IP variants (Alibaba, Oracle) diff --git a/services/platform/convex/lib/http/safe_fetch.test.ts b/services/platform/lib/net/safe-fetch.test.ts similarity index 96% rename from services/platform/convex/lib/http/safe_fetch.test.ts rename to services/platform/lib/net/safe-fetch.test.ts index 9f4dee03fe..fa3d28d603 100644 --- a/services/platform/convex/lib/http/safe_fetch.test.ts +++ b/services/platform/lib/net/safe-fetch.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { isPrivateIp } from './safe_fetch'; +import { isPrivateIp } from './safe-fetch'; describe('lib/http/safe_fetch.isPrivateIp', () => { it.each([ diff --git a/services/platform/convex/lib/http/safe_fetch.ts b/services/platform/lib/net/safe-fetch.ts similarity index 99% rename from services/platform/convex/lib/http/safe_fetch.ts rename to services/platform/lib/net/safe-fetch.ts index 72de08d98f..2d380c9a9a 100644 --- a/services/platform/convex/lib/http/safe_fetch.ts +++ b/services/platform/lib/net/safe-fetch.ts @@ -22,7 +22,7 @@ * provider and any future outbound caller share one audited implementation. */ -import { isPrivateIp } from '../../../lib/shared/net/private-ip'; +import { isPrivateIp } from '../shared/net/private-ip'; export type SafeFetchErrorKind = | 'invalid_url' diff --git a/services/platform/lib/permissions/ability.ts b/services/platform/lib/permissions/ability.ts index a260d2c9b4..60ea0840f2 100644 --- a/services/platform/lib/permissions/ability.ts +++ b/services/platform/lib/permissions/ability.ts @@ -5,7 +5,7 @@ import { } from '@casl/ability'; /** - * Platform resource subjects — matches the Convex RLS table keys in convex/auth.ts. + * Platform resource subjects — matches the server-side access keys in backend/auth/access.ts. */ export type PlatformResource = | 'approvals' @@ -47,7 +47,7 @@ export type AppAbility = MongoAbility<[AppAction, AppSubject]>; /** * Builds a CASL ability instance for the given platform role. - * Mirrors the permission matrix defined in convex/auth.ts. + * Mirrors the permission matrix defined in backend/auth/access.ts. */ export function defineAbilityFor(role: string | null): AppAbility { const { can, cannot, build } = new AbilityBuilder( diff --git a/services/platform/lib/shared/chat-errors.ts b/services/platform/lib/shared/chat-errors.ts index 8eb6b8b457..a60ed7bb98 100644 --- a/services/platform/lib/shared/chat-errors.ts +++ b/services/platform/lib/shared/chat-errors.ts @@ -60,7 +60,7 @@ export function isChatErrorCode(value: unknown): value is ChatErrorCode { * Codes that are a property of the PROVIDER/account, not the specific model: * every model on the same provider would fail the same way deterministically. * The failover loop skips the rest of the provider's models when one of these - * occurs (see `classifyFailureScope` in convex/providers/errors.ts). + * occurs (see `classifyFailureScope` below). * * Transient failures (5xx, overload, timeout, ECONNRESET, 429) are deliberately * NOT here: on an aggregator a sibling model may route to a healthy upstream, diff --git a/services/platform/lib/shared/constants/convex-enums.ts b/services/platform/lib/shared/constants/product-enums.ts similarity index 76% rename from services/platform/lib/shared/constants/convex-enums.ts rename to services/platform/lib/shared/constants/product-enums.ts index a2b4493e26..0e5be4985d 100644 --- a/services/platform/lib/shared/constants/convex-enums.ts +++ b/services/platform/lib/shared/constants/product-enums.ts @@ -1,7 +1,7 @@ /* tslint:disable */ /** - * This file contains Convex-compatible enums for the application. + * Product-status enum vocabulary shared by the product dialogs. */ // Product status types diff --git a/services/platform/lib/shared/file-types.ts b/services/platform/lib/shared/file-types.ts index 9faffa67d4..3ac8df1852 100644 --- a/services/platform/lib/shared/file-types.ts +++ b/services/platform/lib/shared/file-types.ts @@ -456,7 +456,7 @@ export const CHAT_UPLOAD_ALLOWED_TYPES: readonly string[] = [ * subset of {@link CHAT_UPLOAD_ALLOWED_TYPES} that drops audio/video — task * attachments are reference material (screenshots, specs, sheets), not media * that flows through the transcription pipeline. Used for BOTH the client - * upload gate (`useConvexFileUpload({ allowedTypes })`) and the server-side + * upload gate (`useFileUpload({ allowedTypes })`) and the server-side * `validateTaskAttachments` check. Pair with {@link DOCUMENT_UPLOAD_ACCEPT} for * the file-picker `accept` attribute. */ @@ -640,7 +640,7 @@ export interface AttachmentCapsConfig { /** * Generic count/size/MIME cap check shared by every server-side surface that * re-enforces a composer's client-side attachment caps — a scripted client - * bypassing the upload widget's gates (`useConvexFileUpload` and friends) + * bypassing the upload widget's gates (`useFileUpload` and friends) * could otherwise attach an unbounded `attachments[]` to a public mutation. * Each caller supplies its own caps + error codes and stays the single * source of truth for its own limits; this only owns the check ORDER (count, @@ -905,7 +905,7 @@ export function getDocumentPreviewKind( * Extensions (lowercase, no dot) in-process RAG indexing can handle. * * MUST stay in sync with `ALL_SUPPORTED_EXTENSIONS` in - * `services/platform/convex/lib/knowledge/extraction/router.ts` (minus the + * `services/platform/backend/core/lib/knowledge/extraction/router.ts` (minus the * deliberately-unindexed `SENSITIVE_EXTENSIONS` like `.log`). Update both * sides in the same commit. * diff --git a/services/platform/lib/shared/handlers/function-refs.ts b/services/platform/lib/shared/handlers/function-refs.ts index 7edbf21fe8..9f8c1caac3 100644 --- a/services/platform/lib/shared/handlers/function-refs.ts +++ b/services/platform/lib/shared/handlers/function-refs.ts @@ -3,7 +3,7 @@ * * A reused 0.4 body addresses another handler as `internal.a.b.c`. That * expression carries no code and no import — only a NAME — and the 0.5 ctx - * shim (`backend/lib/convex-shim.ts`) dispatches that name to a SQL-backed + * shim (`backend/lib/ctx-shim.ts`) dispatches that name to a SQL-backed * handler. So the whole contract is: how a dotted path becomes a string, and * how a reference gives that string back. * diff --git a/services/platform/lib/utils/convex-error.test.ts b/services/platform/lib/utils/backend-error.test.ts similarity index 100% rename from services/platform/lib/utils/convex-error.test.ts rename to services/platform/lib/utils/backend-error.test.ts diff --git a/services/platform/lib/webdav/auth-parity.test.ts b/services/platform/lib/webdav/auth-parity.test.ts index 5109909b35..34293e8a9f 100644 --- a/services/platform/lib/webdav/auth-parity.test.ts +++ b/services/platform/lib/webdav/auth-parity.test.ts @@ -1,18 +1,17 @@ // Byte-parity guard for the WebDAV HMAC helpers. The crypto primitives -// (hmacHash / timingSafeEqual) are hand-duplicated in lib/webdav/auth.ts -// (Hono / Node side) and convex/webdav/helpers.ts (Convex V8 isolate side) -// because the isolate cannot import from lib/. If the two copies ever -// drift, Basic-auth silently breaks: a password hashed by helpers.ts at -// create time would no longer match the digest auth.ts computes at login. -// This test pins both to the same known-answer vector — the missing test -// that auth.ts's header comment promised. +// (hmacHash / timingSafeEqual) are hand-duplicated in lib/webdav/auth.ts and +// backend/core/webdav/helpers.ts (a split inherited from the retired Convex +// isolate, which could not import from lib/). If the two copies ever drift, +// Basic-auth silently breaks: a password hashed by helpers.ts at create time +// would no longer match the digest auth.ts computes at login. This test pins +// both to the same known-answer vector. import { describe, expect, it } from 'vitest'; import { hmacHash as hmacHashHelpers, timingSafeEqual as timingSafeEqualHelpers, -} from '../../convex/webdav/helpers'; +} from '../../backend/core/webdav/helpers'; import { hmacHash as hmacHashAuth, timingSafeEqual as timingSafeEqualAuth, @@ -28,7 +27,7 @@ const PASSWORD = 'app-pass-1234-5678-90ab'; const EXPECTED = 'a2bbb0a6897a8e7426813c57c6f1bdeeeb45cb8d177e2ff18c22a3f9f6e5ee30'; -describe('webdav HMAC helper parity (auth.ts ↔ convex/webdav/helpers.ts)', () => { +describe('webdav HMAC helper parity (auth.ts ↔ backend/core/webdav/helpers.ts)', () => { it('both hmacHash copies produce the pinned digest', async () => { const fromAuth = await hmacHashAuth(PASSWORD, KEY_HEX); const fromHelpers = await hmacHashHelpers(PASSWORD, KEY_HEX); diff --git a/services/platform/lib/webdav/auth.ts b/services/platform/lib/webdav/auth.ts index 34d899f3a3..517c1a3f80 100644 --- a/services/platform/lib/webdav/auth.ts +++ b/services/platform/lib/webdav/auth.ts @@ -6,12 +6,11 @@ import { type WebDAVRequest, } from './types'; -// MIRROR OF convex/webdav/helpers.ts — keep these in sync. The Convex -// isolate cannot import from lib/, so the `hexToBytes` / `encodeText` / -// `bytesToHex` / `hmacHash` / `timingSafeEqual` helpers are duplicated -// here. If you change one, change both — and update the unit-test -// vector (`lib/webdav/auth.test.ts` once added) that pins them to the -// same output bytes. +// MIRROR OF backend/core/webdav/helpers.ts — keep these in sync. The +// duplication predates the Postgres port (the retired Convex isolate could +// not import from lib/); both sides are Node now, so folding them into one +// copy is possible and worth its own change. Until then: if you change one, +// change both — auth-parity.test.ts pins them to the same output bytes. // Outcome of Basic-auth verification + org-slug resolution. // @@ -126,8 +125,8 @@ function bytesToHex(bytes: Uint8Array): string { } // Exported for the cross-module parity test (auth-parity.test.ts), which -// pins this against the convex/webdav/helpers.ts duplicate. Not part of -// the public auth surface otherwise. +// pins this against the backend/core/webdav/helpers.ts duplicate. Not part +// of the public auth surface otherwise. export async function hmacHash( plaintext: string, secretHex: string, @@ -189,7 +188,7 @@ export async function verifyBasicAuthForDav( // nothing, so legitimate clients never deplete the bucket. const chargeFailure = async (organizationId: string): Promise => { try { - await ctx.convex.mutation( + await ctx.backend.mutation( anyRefs.webdav.app_password_queries.chargeWebdavAuthFailure, { organizationId, clientIp }, ); @@ -223,7 +222,7 @@ export async function verifyBasicAuthForDav( // findCandidatesByPrefix is a read-only internalQuery — it consumes no // rate-limit token. Throttling is charged below, only on a failed // match, so successful auths never deplete the bucket. - const rawCandidates = await ctx.convex.query( + const rawCandidates = await ctx.backend.query( anyRefs.webdav.app_password_queries.findCandidatesByPrefix, { organizationId: orgRow.organizationId, @@ -278,7 +277,7 @@ export async function verifyBasicAuthForDav( const lastTouch = lastUseTouchAt.get(matched._id) ?? 0; if (now - lastTouch > LAST_USE_TOUCH_INTERVAL_MS) { lastUseTouchAt.set(matched._id, now); - void ctx.convex + void ctx.backend .mutation(anyRefs.webdav.app_password_mutations.recordAppPasswordUse, { id: matched._id, at: now, diff --git a/services/platform/lib/webdav/handler.ts b/services/platform/lib/webdav/handler.ts index bf96b8f7d5..a25f8af84c 100644 --- a/services/platform/lib/webdav/handler.ts +++ b/services/platform/lib/webdav/handler.ts @@ -41,7 +41,7 @@ function getHmacSecret(): string { !/^[0-9a-f]+$/i.test(raw) ) { throw new Error( - `WEBDAV_APP_PASSWORD_HMAC_KEY is unset, too short (need >= ${WEBDAV_HMAC_KEY_MIN_LENGTH} hex chars), or non-hex. Set via 'convex env set WEBDAV_APP_PASSWORD_HMAC_KEY=$(openssl rand -hex 32)' and mirror to platform env via docker-entrypoint.`, + `WEBDAV_APP_PASSWORD_HMAC_KEY is unset, too short (need >= ${WEBDAV_HMAC_KEY_MIN_LENGTH} hex chars), or non-hex. It derives from INSTANCE_SECRET automatically; to set it explicitly use WEBDAV_APP_PASSWORD_HMAC_KEY=$(openssl rand -hex 32) in the environment.`, ); } cachedHmacSecret = raw; @@ -87,7 +87,7 @@ export async function dispatch( const authResult = await verifyBasicAuthForDav(req, ctx, parsed.orgSlug, { hmacSecret, resolveOrgAndMembership: async (orgSlug, userId) => { - const r = await ctx.convex.query( + const r = await ctx.backend.query( anyRefs.webdav.org_queries.resolveOrgAndCheckMembership, { orgSlug, userId }, ); diff --git a/services/platform/lib/webdav/locks.ts b/services/platform/lib/webdav/locks.ts index 49f52c6556..ea4b4c3c94 100644 --- a/services/platform/lib/webdav/locks.ts +++ b/services/platform/lib/webdav/locks.ts @@ -64,7 +64,7 @@ export async function checkCollectionDescendantLocks( parsed: Pick, ): Promise { const clauses = parseIfHeader(req.headers.get('if')); - const raw: unknown = await ctx.convex.query( + const raw: unknown = await ctx.backend.query( anyRefs.webdav.lock_queries.findLocksUnderPath, { organizationId: auth.organizationId, @@ -109,14 +109,14 @@ async function runLockCheck( const clauses = parseIfHeader(req.headers.get('if')); for (const { path, requireInfinity } of candidates) { - const found = await ctx.convex.query( + const found = await ctx.backend.query( anyRefs.webdav.lock_queries.findLockForPath, { organizationId: auth.organizationId, resourcePath: path }, ); if (found?.expiredId) { // Fire-and-forget eviction. Lazy cleanup pattern — don't await. - void ctx.convex + void ctx.backend .mutation(anyRefs.webdav.lock_mutations.deleteLockIfStale, { id: found.expiredId, }) diff --git a/services/platform/lib/webdav/methods/delete.ts b/services/platform/lib/webdav/methods/delete.ts index 532cc983d0..8147628019 100644 --- a/services/platform/lib/webdav/methods/delete.ts +++ b/services/platform/lib/webdav/methods/delete.ts @@ -36,7 +36,7 @@ export async function handleDelete( }; } - const resolved = await ctx.convex.query( + const resolved = await ctx.backend.query( anyRefs.webdav.tree_queries.resolvePath, { organizationId: auth.organizationId, @@ -50,7 +50,7 @@ export async function handleDelete( try { if (resolved.kind === 'document') { - await ctx.convex.mutation( + await ctx.backend.mutation( anyRefs.webdav.tree_mutations.softDeleteDocument, { organizationId: auth.organizationId, @@ -74,7 +74,7 @@ export async function handleDelete( body: descendantLock.body, }; } - await ctx.convex.mutation( + await ctx.backend.mutation( anyRefs.webdav.tree_mutations.deleteFolderCascade, { organizationId: auth.organizationId, @@ -114,7 +114,7 @@ export async function handleDelete( // Removing a resource removes its locks (RFC 4918 §9.6.1) — drop the // lock row(s) for this path and any descendants so a stale lock can't // 423 a later recreate of the same name. - await ctx.convex + await ctx.backend .mutation(anyRefs.webdav.lock_mutations.deleteLocksUnderPath, { organizationId: auth.organizationId, resourcePath: lockKeyFromParsed(parsed), diff --git a/services/platform/lib/webdav/methods/get.ts b/services/platform/lib/webdav/methods/get.ts index 38f353e5b4..b4feffe09c 100644 --- a/services/platform/lib/webdav/methods/get.ts +++ b/services/platform/lib/webdav/methods/get.ts @@ -192,7 +192,7 @@ export async function handleGet( headOnly: boolean, req?: WebDAVRequest, ): Promise { - const resolved = await ctx.convex.query( + const resolved = await ctx.backend.query( anyRefs.webdav.tree_queries.resolvePath, { organizationId: auth.organizationId, @@ -214,7 +214,7 @@ export async function handleGet( }; } - const doc = await ctx.convex.query( + const doc = await ctx.backend.query( anyRefs.webdav.tree_queries.getDocumentProps, { organizationId: auth.organizationId, @@ -301,7 +301,7 @@ export async function handleGet( // The /storage httpAction (ctx.storage.get) buffers the whole blob in the // isolate and caps at its memory limit — keep it only as a fallback for // deployments where the direct URL isn't reachable from this process. - const directUrl: unknown = await ctx.convex + const directUrl: unknown = await ctx.backend .query(anyRefs.webdav.tree_queries.getWebdavBlobUrl, { storageId: doc.fileId, }) @@ -316,7 +316,7 @@ export async function handleGet( // unreachable from this container; re-home onto the reachable backend // origin so the fast streaming path works in compose (no :3211 fallback). upstream = await fetchBlob( - rewriteStorageOrigin(directUrl, ctx.convexApiUrl), + rewriteStorageOrigin(directUrl, ctx.backendApiUrl), 'direct', ); } diff --git a/services/platform/lib/webdav/methods/lock.ts b/services/platform/lib/webdav/methods/lock.ts index a158d7358c..1ff41485a3 100644 --- a/services/platform/lib/webdav/methods/lock.ts +++ b/services/platform/lib/webdav/methods/lock.ts @@ -77,7 +77,7 @@ export async function handleLock( } try { for (const token of tokens) { - const existing = await ctx.convex.query( + const existing = await ctx.backend.query( anyRefs.webdav.lock_queries.findLockByToken, { token }, ); @@ -95,7 +95,7 @@ export async function handleLock( ) { continue; } - const refreshed = await ctx.convex.mutation( + const refreshed = await ctx.backend.mutation( anyRefs.webdav.lock_mutations.refreshLock, { lockToken: token, @@ -133,7 +133,7 @@ export async function handleLock( // RFC §9.10.4 + §7.3: LOCK on a non-existent URI creates an empty // resource bound to the lock — clients use this to reserve a name // before a PUT. Status code must be 201 Created in that case. - const resolved = await ctx.convex.query( + const resolved = await ctx.backend.query( anyRefs.webdav.tree_queries.resolvePath, { organizationId: auth.organizationId, @@ -154,7 +154,7 @@ export async function handleLock( const lockKey = lockKeyFromParsed(parsed); try { - await ctx.convex.mutation(anyRefs.webdav.lock_mutations.createLock, { + await ctx.backend.mutation(anyRefs.webdav.lock_mutations.createLock, { organizationId: auth.organizationId, resourcePath: lockKey, lockToken, diff --git a/services/platform/lib/webdav/methods/mkcol.ts b/services/platform/lib/webdav/methods/mkcol.ts index 9f63fc7dd6..54cc60340a 100644 --- a/services/platform/lib/webdav/methods/mkcol.ts +++ b/services/platform/lib/webdav/methods/mkcol.ts @@ -60,7 +60,7 @@ export async function handleMkcol( const name = parsed.segments[parsed.segments.length - 1]; try { - await ctx.convex.mutation(anyRefs.webdav.tree_mutations.mkcol, { + await ctx.backend.mutation(anyRefs.webdav.tree_mutations.mkcol, { organizationId: auth.organizationId, parentSegments, name, diff --git a/services/platform/lib/webdav/methods/move.ts b/services/platform/lib/webdav/methods/move.ts index 769ae6c3e7..e8b4056b0c 100644 --- a/services/platform/lib/webdav/methods/move.ts +++ b/services/platform/lib/webdav/methods/move.ts @@ -153,7 +153,7 @@ async function doMoveOrCopy( } } - const src = await ctx.convex.query(anyRefs.webdav.tree_queries.resolvePath, { + const src = await ctx.backend.query(anyRefs.webdav.tree_queries.resolvePath, { organizationId: auth.organizationId, namespace: parsed.namespace, segments: parsed.segments, @@ -189,7 +189,7 @@ async function doMoveOrCopy( overwrite, userId: auth.userId, }; - const result = await ctx.convex.mutation( + const result = await ctx.backend.mutation( op === 'MOVE' ? anyRefs.webdav.tree_mutations.moveResource : anyRefs.webdav.tree_mutations.copyResource, @@ -201,7 +201,7 @@ async function doMoveOrCopy( // (RFC 4918 §9.9: MOVE relocates the resource and its locks don't // follow in v1). COPY leaves the source intact, so nothing to clean. if (op === 'MOVE') { - await ctx.convex + await ctx.backend .mutation(anyRefs.webdav.lock_mutations.deleteLocksUnderPath, { organizationId: auth.organizationId, resourcePath: lockKeyFromParsed(parsed), diff --git a/services/platform/lib/webdav/methods/propfind.ts b/services/platform/lib/webdav/methods/propfind.ts index 1e959a661a..bf1b9e784c 100644 --- a/services/platform/lib/webdav/methods/propfind.ts +++ b/services/platform/lib/webdav/methods/propfind.ts @@ -65,7 +65,7 @@ export async function handlePropfind( const propfindRequest = parsePropfindBody(bodyText); // Resolve the URL to a node — root, folder, or document. - const resolved = await ctx.convex.query( + const resolved = await ctx.backend.query( anyRefs.webdav.tree_queries.resolvePath, { organizationId: auth.organizationId, @@ -111,7 +111,7 @@ export async function handlePropfind( index: props.length - 1, }); } else { - const doc = await ctx.convex.query( + const doc = await ctx.backend.query( anyRefs.webdav.tree_queries.getDocumentProps, { organizationId: auth.organizationId, @@ -132,7 +132,7 @@ export async function handlePropfind( if (depth === 1 && (resolved.kind === 'root' || resolved.kind === 'folder')) { const folderId = resolved.kind === 'folder' ? resolved.folderId : null; - const listing = await ctx.convex.query( + const listing = await ctx.backend.query( anyRefs.webdav.tree_queries.listCollection, { organizationId: auth.organizationId, @@ -211,7 +211,7 @@ export async function handlePropfind( await Promise.all( lookups.map(async ({ path, index }) => { try { - const result = await ctx.convex.query( + const result = await ctx.backend.query( anyRefs.webdav.lock_queries.findLockForPath, { organizationId: auth.organizationId, diff --git a/services/platform/lib/webdav/methods/proppatch.ts b/services/platform/lib/webdav/methods/proppatch.ts index b0ad044a4d..b0f3337be9 100644 --- a/services/platform/lib/webdav/methods/proppatch.ts +++ b/services/platform/lib/webdav/methods/proppatch.ts @@ -46,7 +46,7 @@ export async function handleProppatch( return { status: 403, headers: {}, body: 'Trash is read-only' }; } - const resolved = await ctx.convex.query( + const resolved = await ctx.backend.query( anyRefs.webdav.tree_queries.resolvePath, { organizationId: auth.organizationId, diff --git a/services/platform/lib/webdav/methods/put.ts b/services/platform/lib/webdav/methods/put.ts index 643ec71844..368f88a9ce 100644 --- a/services/platform/lib/webdav/methods/put.ts +++ b/services/platform/lib/webdav/methods/put.ts @@ -47,7 +47,7 @@ export async function handlePut( } // Pre-check existence to choose 201 vs 204 (RFC 4918 §9.7.1). - const resolved = await ctx.convex.query( + const resolved = await ctx.backend.query( anyRefs.webdav.tree_queries.resolvePath, { organizationId: auth.organizationId, @@ -72,7 +72,7 @@ export async function handlePut( ifMatch !== null || (ifNoneMatch !== null && ifNoneMatch.trim() !== '*')) ) { - const props = await ctx.convex.query( + const props = await ctx.backend.query( anyRefs.webdav.tree_queries.getDocumentProps, { organizationId: auth.organizationId, documentId: resolved.documentId }, ); @@ -158,7 +158,7 @@ export async function handlePut( // endpoint — leave it untouched. See ctx.ts. let uploadTarget: { url: string; method: 'POST' | 'PUT'; s3Ref?: string }; if (declaredSize !== null) { - const handoff: unknown = await ctx.convex.action( + const handoff: unknown = await ctx.backend.action( anyRefs.files.blob_actions.generateWebdavBlobUpload, { organizationId: auth.organizationId, contentType }, ); @@ -171,7 +171,7 @@ export async function handlePut( } uploadTarget = handoff; } else { - const rawUploadUrl: unknown = await ctx.convex.mutation( + const rawUploadUrl: unknown = await ctx.backend.mutation( anyRefs.webdav.tree_mutations.generateWebdavUploadUrl, {}, ); @@ -186,7 +186,7 @@ export async function handlePut( } const uploadUrl = uploadTarget.method === 'POST' - ? rewriteStorageOrigin(uploadTarget.url, ctx.convexApiUrl) + ? rewriteStorageOrigin(uploadTarget.url, ctx.backendApiUrl) : uploadTarget.url; // Wrap the body in a counter so we can fail the request if the @@ -254,7 +254,7 @@ export async function handlePut( const xOcMtime = parseMtimeHeader(req.headers.get('x-oc-mtime')); try { - const result = await ctx.convex.mutation( + const result = await ctx.backend.mutation( anyRefs.webdav.tree_mutations.ingestPutBlob, { organizationId: auth.organizationId, @@ -277,7 +277,7 @@ export async function handlePut( // to avoid a permanent _storage leak (a missing-parent PUT is a common // sync-client race). Fire-and-forget — the client still gets the real // error below. - void ctx.convex + void ctx.backend .mutation(anyRefs.webdav.tree_mutations.deleteWebdavBlob, { storageId, organizationId: auth.organizationId, diff --git a/services/platform/lib/webdav/methods/unlock.ts b/services/platform/lib/webdav/methods/unlock.ts index b0637cec09..8fe2143900 100644 --- a/services/platform/lib/webdav/methods/unlock.ts +++ b/services/platform/lib/webdav/methods/unlock.ts @@ -34,7 +34,7 @@ export async function handleUnlock( } try { - await ctx.convex.mutation(anyRefs.webdav.lock_mutations.releaseLock, { + await ctx.backend.mutation(anyRefs.webdav.lock_mutations.releaseLock, { lockToken: token, ownerUserId: auth.userId, organizationId: auth.organizationId, diff --git a/services/platform/lib/webdav/test-helpers.ts b/services/platform/lib/webdav/test-helpers.ts index a8720b87c1..aa2be69ea5 100644 --- a/services/platform/lib/webdav/test-helpers.ts +++ b/services/platform/lib/webdav/test-helpers.ts @@ -55,7 +55,7 @@ export function makeStubCtx(overrides: StubOverrides = {}): WebDAVCtx { return null; }, // findCandidatesByPrefix is a read-only internalQuery (auth.ts calls - // ctx.convex.query). Default returns the one valid candidate whose + // ctx.backend.query). Default returns the one valid candidate whose // hashed password matches our test password. 'webdav/app_password_queries:findCandidatesByPrefix': async () => { return [ @@ -95,7 +95,7 @@ export function makeStubCtx(overrides: StubOverrides = {}): WebDAVCtx { return Promise.resolve(handler(args)); }; - const fakeConvex: WebDAVBackend = { + const fakeBackend: WebDAVBackend = { query: (ref: unknown, args?: unknown) => dispatchByName(queries, ref, args), mutation: (ref: unknown, args?: unknown) => dispatchByName(mutations, ref, args), @@ -107,9 +107,9 @@ export function makeStubCtx(overrides: StubOverrides = {}): WebDAVCtx { }; return { - convex: fakeConvex, + backend: fakeBackend, storageBaseUrl: 'http://localhost:3211', - convexApiUrl: 'http://localhost:3210', + backendApiUrl: 'http://localhost:3210', }; } @@ -197,8 +197,8 @@ function bytesToHex(bytes: Uint8Array): string { return s; } -// Re-export so tests can compare against the same code referenced in -// move/etc. without re-importing convex/values directly. +// Re-export so tests can compare against the same error type move/etc. +// throw without importing it from a second path. export { AppError }; // Read a WebDAVResponse body as text. Handlers occasionally return diff --git a/services/platform/lib/webdav/types.ts b/services/platform/lib/webdav/types.ts index e98e61c791..bf606a4175 100644 --- a/services/platform/lib/webdav/types.ts +++ b/services/platform/lib/webdav/types.ts @@ -135,20 +135,14 @@ export interface WebDAVBackend { // Shared ctx threaded into every dispatch — built once at server start. export interface WebDAVCtx { - convex: WebDAVBackend; + backend: WebDAVBackend; // Public base URL used to materialize blob fetch URLs for GET (we - // proxy through Convex /storage). Falls back to convex client's URL. + // proxy legacy `_storage` blobs through the backend). storageBaseUrl: string; - // Token used to call /storage from the platform server (same as the - // Convex deployment URL — bearer auth not required for /storage since - // the storageId itself is hard to guess). - - // Backend API origin (CONVEX_URL, :3210). `ctx.storage.generateUploadUrl()` - // / `getUrl()` return URLs carrying the backend's *self-reported* origin - // (`http://127.0.0.1:3210` self-hosted), which is unreachable from inside the - // platform container (Convex is a separate container, `http://convex:3210`). + // Backend API origin. Legacy `_storage` upload/get URLs carry a + // self-reported origin that may be unreachable from this process; // PUT/GET re-home those URLs onto this origin via `rewriteStorageOrigin`. - convexApiUrl: string; + backendApiUrl: string; } export interface ParsedPath { diff --git a/services/platform/scripts/dev-engine.ts b/services/platform/scripts/dev-engine.ts index 9765323c7e..db511463c0 100644 --- a/services/platform/scripts/dev-engine.ts +++ b/services/platform/scripts/dev-engine.ts @@ -233,7 +233,7 @@ async function provisionVideoToolchain(): Promise { } try { const { ensureVideoToolchain } = - await import('../convex/video_links/ytdlp_toolchain'); + await import('../backend/core/video_links/ytdlp_toolchain'); const tc = await ensureVideoToolchain(); process.env.VIDEO_INGEST_BIN_DIR ||= tc.binDir; process.env.VIDEO_INGEST_FFMPEG_LOCATION ||= tc.ffmpegLocation; diff --git a/services/platform/scripts/validate-builtin-configs.ts b/services/platform/scripts/validate-builtin-configs.ts index 43238ce979..27741cdae9 100644 --- a/services/platform/scripts/validate-builtin-configs.ts +++ b/services/platform/scripts/validate-builtin-configs.ts @@ -28,7 +28,7 @@ import { loadHarnesses, loadProviderDefinitions, loadStaticCatalogs, -} from '../convex/lib/providers/load_system_config'; +} from '../backend/core/lib/providers/load_system_config'; import { loadConnectorDefinitions } from '../lib/connectors/catalog'; // scripts/ -> services/platform -> services -> repo root -> configs/platform/system diff --git a/services/platform/vitest.ui.config.ts b/services/platform/vitest.ui.config.ts index 17850ab024..8cde6c6c94 100644 --- a/services/platform/vitest.ui.config.ts +++ b/services/platform/vitest.ui.config.ts @@ -53,7 +53,7 @@ export default defineConfig({ 'node_modules', '.next', 'dist', - 'convex/**', + 'backend/**', '**/*.browser.test.{ts,tsx}', ], deps: { diff --git a/services/sandbox/src/wire.ts b/services/sandbox/src/wire.ts index 79aacb8b0e..837cdb46a2 100644 --- a/services/sandbox/src/wire.ts +++ b/services/sandbox/src/wire.ts @@ -1,18 +1,13 @@ // Wire-protocol enums + literals shared between server.ts, spawn.ts, and -// the response builder. Mirrors `services/platform/convex/sandbox/wire.ts` -// on the Convex side — the spawner cannot import from Convex (different -// runtime, different package), so this is a parallel file. Both ends must -// stay in sync; the platform side carries a compile-time `satisfies` -// assertion (see `convex/node_only/sandbox/helpers/spawner_client.ts`) -// that asserts these literals are a subset of the Convex `sandboxRunStatusLiterals` -// / `sandboxErrorCodeLiterals` / `sandboxPhaseEventLiterals` arrays, so a -// drift on either side fails the CI typecheck. +// the response builder. This file is the contract's home: the platform's +// session-exec drivers (services/platform/backend/core/node_only/sandbox/) +// parse these phase/SSE/error strings structurally off the stream, and the +// platform integration suite drives a fake spawner speaking exactly this +// protocol — that suite is what catches a drift. -// `sandboxRunStatusLiterals` lives only on the Convex side -// (`services/platform/convex/sandbox/wire.ts`) — the spawner never emits a -// run-status string, only phase events + a final result with one of three -// terminal `status` values (`completed | failed | cancelled`). Kept off -// this file deliberately so unused-export sweeps stay clean. +// There is deliberately no run-status vocabulary here — the spawner never +// emits a run-status string, only phase events + a final result with one of +// three terminal `status` values (`completed | failed | cancelled`). export const sandboxErrorCodeLiterals = [ 'TIMEOUT', @@ -39,9 +34,8 @@ export const sandboxErrorCodeLiterals = [ // Pre-stage attestation failure raised by the platform when // `ExecuteResponse.priorStage.skipped` shows files the platform expected // to inject didn't actually make it onto `/agent/output/`. The - // spawner never emits this code itself — it's an action-side gate — but - // the literal lives here so the parity guard on the Convex side stays - // satisfied. + // spawner never emits this code itself — it's a platform-side gate — but + // the literal lives here so the vocabulary stays complete in one place. 'PRE_STAGE_FAILED', // Output-pipeline completeness gate: the action treats any non-empty // `uploadStats.failures` as fatal so a partially-harvested workspace @@ -73,9 +67,6 @@ export type SandboxErrorCode = (typeof sandboxErrorCodeLiterals)[number]; * - `error` — zero or one SSE-side transport error (e.g. spawn aborted * before a result was produced). * - * The convex side has a compile-time parity guard - * (services/platform/convex/sandbox/wire.ts) that fails CI typecheck if - * either side drifts. */ export const sandboxSseEventLiterals = [ 'phase', @@ -95,8 +86,7 @@ export const ORG_ID_ALPHABET_RE = /^[a-zA-Z0-9_-]{1,128}$/; // --------------------------------------------------------------------------- // Persistent sessions (sessions plan, milestone A). The `/v1/sessions` API is // a sibling of the one-shot `/v1/execute` path; literals live here so the -// Convex-side mirror (`convex/sandbox/wire.ts`) can keep its compile-time -// parity guard over a single import surface. +// whole wire vocabulary has a single import surface. // --------------------------------------------------------------------------- /** diff --git a/tools/cli/scripts/generate-embedded.ts b/tools/cli/scripts/generate-embedded.ts index fdcde5b941..f22c960630 100644 --- a/tools/cli/scripts/generate-embedded.ts +++ b/tools/cli/scripts/generate-embedded.ts @@ -19,7 +19,7 @@ const REPO_ROOT = resolve(CLI_ROOT, '../..'); // `update` actions key their reference tree (`.tale/reference/builtin-configs/`) // on that label, and the catalog's `/...` shape is unchanged. const REFERENCE_DIRS: [string, string][] = [ - ['services/platform/convex', 'convex'], + ['services/platform/backend/core', 'backend/core'], ['services/platform/lib', 'lib'], ['configs/platform/custom', 'builtin-configs'], ]; From e9ea0b9e2b185d2576d250b105da52d37ccd11e3 Mon Sep 17 00:00:00 2001 From: larryro <371767072@qq.com> Date: Tue, 1 Sep 2026 10:34:22 +0800 Subject: [PATCH 3/5] refactor(platform): drop the convex packages and dead client remnants --- .commitlintrc.json | 1 - bun.lock | 11 - package.json | 2 - patches/@convex-dev%2Fagent@0.6.1.patch | 27 - patches/convex-helpers@0.1.114.patch | 13 - .../ui/data-display/email-preview.test.tsx | 13 +- .../ui/data-display/email-preview.tsx | 6 - .../chat-health-metrics-page.test.tsx | 19 +- .../upload-automation-dialog.test.tsx | 16 - .../contacts/hooks/mutation-hooks.test.ts | 16 - .../hooks/mutation-hooks.test.ts | 16 - .../components/breadcrumb-navigation.test.tsx | 14 - .../components/create-folder-dialog.test.tsx | 4 - .../components/document-row-actions.test.tsx | 4 - .../document-team-tags-dialog.test.tsx | 4 - .../components/rag-status-badge.test.tsx | 4 - .../documents/hooks/mutation-hooks.test.ts | 14 - .../hooks/upload-with-progress.test.ts | 2 - .../onboarding/steps/workspace-step.test.tsx | 5 - .../organization/hooks/mutation-hooks.test.ts | 21 - .../teams/hooks/mutation-hooks.test.ts | 14 - .../shared/markdown/code-block.test.tsx | 4 - .../hooks/use-auth-from-better-auth.test.tsx | 223 - .../app/hooks/use-auth-from-better-auth.ts | 160 - .../hooks/use-session-idle-watchdog.test.tsx | 4 - .../platform/app/hooks/use-session-user.ts | 8 +- .../app/lib/auth/convex-token-cache.test.ts | 144 - .../app/lib/auth/convex-token-cache.ts | 178 - .../app/lib/auth/session-query.test.ts | 16 +- .../platform/app/lib/auth/session-query.ts | 7 - .../platform/app/lib/org-error-recovery.ts | 11 +- services/platform/app/routes/_auth.tsx | 7 +- .../platform/app/routes/dashboard/$id.tsx | 12 +- .../dashboard/dashboard-layout.test.tsx | 16 - .../core/lib/rls/MEMBERSHIP_MIRROR_DESIGN.md | 3843 ----------------- services/platform/convex.json | 24 - services/platform/lib/auth-client.ts | 2 - .../lib/shared/handlers/function-refs.test.ts | 71 +- services/platform/messages/de.yml | 3 - services/platform/messages/en.yml | 3 - services/platform/messages/fr.yml | 3 - services/platform/package.json | 2 - 42 files changed, 72 insertions(+), 4895 deletions(-) delete mode 100644 patches/@convex-dev%2Fagent@0.6.1.patch delete mode 100644 patches/convex-helpers@0.1.114.patch delete mode 100644 services/platform/app/hooks/use-auth-from-better-auth.test.tsx delete mode 100644 services/platform/app/hooks/use-auth-from-better-auth.ts delete mode 100644 services/platform/app/lib/auth/convex-token-cache.test.ts delete mode 100644 services/platform/app/lib/auth/convex-token-cache.ts delete mode 100644 services/platform/backend/core/lib/rls/MEMBERSHIP_MIRROR_DESIGN.md delete mode 100644 services/platform/convex.json diff --git a/.commitlintrc.json b/.commitlintrc.json index 56286fb853..226ef68003 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -8,7 +8,6 @@ "ai", "claude", "cli", - "convex", "db", "deps", "designs", diff --git a/bun.lock b/bun.lock index 4404d4aa58..71f36ce61b 100644 --- a/bun.lock +++ b/bun.lock @@ -183,7 +183,6 @@ "@better-auth/api-key": "1.6.23", "@better-auth/passkey": "1.6.23", "@casl/ability": "6.8.0", - "@convex-dev/better-auth": "0.12.2", "@dnd-kit/core": "6.3.1", "@dnd-kit/sortable": "10.0.0", "@dnd-kit/utilities": "3.2.2", @@ -231,7 +230,6 @@ "chokidar": "5.0.0", "class-variance-authority": "0.7.1", "clsx": "2.1.1", - "convex": "1.35.1", "cron-parser": "5.5.0", "date-fns": "4.1.0", "dayjs": "1.11.20", @@ -449,7 +447,6 @@ "core-js-pure", ], "patchedDependencies": { - "convex-helpers@0.1.114": "patches/convex-helpers@0.1.114.patch", "linkedom@0.18.13": "patches/linkedom@0.18.13.patch", }, "overrides": { @@ -865,8 +862,6 @@ "@conventional-changelog/template": ["@conventional-changelog/template@1.4.0", "", {}, "sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg=="], - "@convex-dev/better-auth": ["@convex-dev/better-auth@0.12.2", "", { "dependencies": { "@better-fetch/fetch": "^1.1.18", "common-tags": "^1.8.2", "convex-helpers": "^0.1.95", "jose": "^6.1.0", "remeda": "^2.32.0", "semver": "^7.7.3", "type-fest": "^5.0.0", "zod": "^4.0.0" }, "peerDependencies": { "better-auth": ">=1.6.9 <1.7.0", "convex": "^1.25.0", "react": "^18.3.1 || ^19.0.0" } }, "sha512-6L8LkXCB5rp9XmQplRj2EVNeD6mkG0b5PPQpm9fooEJ/L3ThGN4jRE4oMfWeBs+9E20eBciWVP0HJooemSgS0w=="], - "@cronvel/get-pixels": ["@cronvel/get-pixels@3.4.1", "", { "dependencies": { "jpeg-js": "^0.4.4", "ndarray": "^1.0.19", "ndarray-pack": "^1.1.1", "node-bitmap": "0.0.1", "omggif": "^1.0.10", "pngjs": "^6.0.0" } }, "sha512-gB5C5nDIacLUdsMuW8YsM9SzK3vaFANe4J11CVXpovpy7bZUGrcJKmc6m/0gWG789pKr6XSZY2aEetjFvSRw5g=="], "@csstools/color-helpers": ["@csstools/color-helpers@6.1.0", "", {}, "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg=="], @@ -2571,10 +2566,6 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "convex": ["convex@1.35.1", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0", "ws": "8.18.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "@clerk/react": "^6.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "@clerk/react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-g23KrTjBiXqRHzWIN0PVFagKjrmFxWUaOSiBsAWPTpXX2rXl0L1F4PR0YpAcMJEzMgfZR9AGymJvLTM+KA6lsQ=="], - - "convex-helpers": ["convex-helpers@0.1.114", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "convex": "^1.32.0", "hono": "^4.0.5", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "typescript": "^5.5", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@standard-schema/spec", "hono", "react", "typescript", "zod"], "bin": { "convex-helpers": "bin.cjs" } }, "sha512-elEdh+gG6BDv2dWIWVvBeJPbHnDQS5+WexUuwlGVJXz1EbMkXz/UIQwFIfLMZIXUwW6ot4JYf/1JJKNStrE6lg=="], - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "cookie-es": ["cookie-es@2.0.1", "", {}, "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA=="], @@ -4085,8 +4076,6 @@ "remarkable": ["remarkable@2.0.1", "", { "dependencies": { "argparse": "^1.0.10", "autolinker": "^3.11.0" }, "bin": { "remarkable": "bin/remarkable.js" } }, "sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA=="], - "remeda": ["remeda@2.39.0", "", {}, "sha512-3Ki8dU1o3OVu4dwIQ2Pj+yiuP7OnEbmWAGmJ3yDRqopily5jsj8NWzPvbS89H85d6UdONKEcUnrfuHY6jN9vyw=="], - "repeat-string": ["repeat-string@1.6.1", "", {}, "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], diff --git a/package.json b/package.json index 62c0a1f99c..24e33654d4 100644 --- a/package.json +++ b/package.json @@ -161,8 +161,6 @@ }, "packageManager": "bun@1.3.10", "patchedDependencies": { - "convex-helpers@0.1.114": "patches/convex-helpers@0.1.114.patch", - "@convex-dev/agent@0.6.1": "patches/@convex-dev%2Fagent@0.6.1.patch", "linkedom@0.18.13": "patches/linkedom@0.18.13.patch" }, "scarfSettings": { diff --git a/patches/@convex-dev%2Fagent@0.6.1.patch b/patches/@convex-dev%2Fagent@0.6.1.patch deleted file mode 100644 index bd7f92f964..0000000000 --- a/patches/@convex-dev%2Fagent@0.6.1.patch +++ /dev/null @@ -1,27 +0,0 @@ -diff --git a/dist/client/streaming.js b/dist/client/streaming.js -index b96123e5bd0934a522ca176416112dce99b313a8..db148f25d851c11376039d4e40e7bf321747b829 100644 ---- a/dist/client/streaming.js -+++ b/dist/client/streaming.js -@@ -294,6 +294,22 @@ export function compressUIMessageChunks(parts) { - compressed.push(part); - } - } -+ else if (part.type === "tool-input-delta") { -+ // Tale patch: coalesce consecutive tool-input-delta parts with -+ // the same toolCallId. Mirrors the text-delta merge above. -+ // Without this, large artifact_create / artifact_edit tool inputs -+ // (10s of KB) produce hundreds of streamDeltas rows, and the -+ // frontend's useStreamingUIMessages (which rebuilds the -+ // UIMessage from cursor=0 on every Convex push) burns O(N²) -+ // main-thread time and freezes the chat UI. Submit upstream; -+ // drop this patch on the next SDK bump once merged. -+ if (last?.type === "tool-input-delta" && part.toolCallId === last.toolCallId) { -+ last.inputTextDelta += part.inputTextDelta; -+ } -+ else { -+ compressed.push(part); -+ } -+ } - else { - compressed.push(part); - } diff --git a/patches/convex-helpers@0.1.114.patch b/patches/convex-helpers@0.1.114.patch deleted file mode 100644 index da31dacfcb..0000000000 --- a/patches/convex-helpers@0.1.114.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/bin.cjs b/bin.cjs -index 5e07847..f145feb 100755 ---- a/bin.cjs -+++ b/bin.cjs -@@ -4650,7 +4650,7 @@ components: - description: Token of the format "Bearer {token}" for normal authentication and "Convex {token}" for admin tokens. - schemas: - ${reindent( -- functionSpec.functions.filter((f) => f.functionType !== "HttpAction").map((f) => generateEndpointSchemas(f)).join("\n"), -+ functionSpec.functions.filter((f) => f.functionType !== "HttpAction").filter((f) => includeInternal || f.visibility.kind === "public").map((f) => generateEndpointSchemas(f)).join("\n"), - 1 - )} - FailedResponse: diff --git a/services/platform/app/components/ui/data-display/email-preview.test.tsx b/services/platform/app/components/ui/data-display/email-preview.test.tsx index e54a1317e3..1cf09e0ed7 100644 --- a/services/platform/app/components/ui/data-display/email-preview.test.tsx +++ b/services/platform/app/components/ui/data-display/email-preview.test.tsx @@ -77,16 +77,13 @@ describe('rewriteExternalImageSrcs', () => { expect(result).toBe(html); }); - it('does not rewrite Convex storage URLs', () => { + it('proxies retired-platform storage URLs like any other external image', () => { + // .convex.cloud/.convex.site used to be allowlisted; the platforms behind + // them are retired, so such a URL is just an external host now. const html = ''; const result = rewriteExternalImageSrcs(html, proxyBase); - expect(result).toBe(html); - }); - - it('does not rewrite Convex site URLs', () => { - const html = ''; - const result = rewriteExternalImageSrcs(html, proxyBase); - expect(result).toBe(html); + expect(result).toContain('/api/image-proxy?url='); + expect(result).not.toBe(html); }); it('does not rewrite cid: references', () => { diff --git a/services/platform/app/components/ui/data-display/email-preview.tsx b/services/platform/app/components/ui/data-display/email-preview.tsx index 71204304bc..2f699fe34e 100644 --- a/services/platform/app/components/ui/data-display/email-preview.tsx +++ b/services/platform/app/components/ui/data-display/email-preview.tsx @@ -240,12 +240,6 @@ export function rewriteExternalImageSrcs( try { const parsed = new URL(srcUrl); if (parsed.origin === proxyOrigin) return _match; - if ( - parsed.hostname.endsWith('.convex.cloud') || - parsed.hostname.endsWith('.convex.site') - ) { - return _match; - } } catch { return _match; } diff --git a/services/platform/app/features/analytics/chat-health/chat-health-metrics-page.test.tsx b/services/platform/app/features/analytics/chat-health/chat-health-metrics-page.test.tsx index d79bf29d14..2670e26ea7 100644 --- a/services/platform/app/features/analytics/chat-health/chat-health-metrics-page.test.tsx +++ b/services/platform/app/features/analytics/chat-health/chat-health-metrics-page.test.tsx @@ -49,17 +49,14 @@ const fixtures = vi.hoisted(() => ({ }, })); -vi.mock('@/app/hooks/use-backend-query', async () => { - const { getFunctionName } = await import('convex/server'); - return { - useBackendQuery: (fn: never) => ({ - data: getFunctionName(fn).includes('getGuardrailStats') - ? fixtures.guardrails - : fixtures.health, - isLoading: false, - }), - }; -}); +vi.mock('@/app/hooks/use-backend-query', () => ({ + useBackendQuery: (name: string) => ({ + data: name.includes('getGuardrailStats') + ? fixtures.guardrails + : fixtures.health, + isLoading: false, + }), +})); describe('ChatHealthMetricsPage', () => { it('renders the title, period control, cards, breakdowns, and guardrail stats', () => { diff --git a/services/platform/app/features/automations/components/upload-automation-dialog.test.tsx b/services/platform/app/features/automations/components/upload-automation-dialog.test.tsx index 33c1b8c37c..321dc21ee0 100644 --- a/services/platform/app/features/automations/components/upload-automation-dialog.test.tsx +++ b/services/platform/app/features/automations/components/upload-automation-dialog.test.tsx @@ -51,22 +51,6 @@ vi.mock('../hooks/mutations', () => ({ useDeployAutomation: () => ({ mutate: deployMutate, isPending: false }), })); -// The generated api proxies are plain objects here; the useMutation mock keys -// off the reference identity strings below. -vi.mock('@/convex/_generated/api', () => ({ - api: { - automations: { - upload_action: { uploadAutomation: 'automations/upload_action' }, - upload_mutations: { - generateAutomationUploadUrl: - 'automations/upload_mutations:generateAutomationUploadUrl', - recordAutomationUploadIntent: - 'automations/upload_mutations:recordAutomationUploadIntent', - }, - }, - }, -})); - import { UploadAutomationDialog } from './upload-automation-dialog'; const fetchMock = vi.fn(); diff --git a/services/platform/app/features/contacts/hooks/mutation-hooks.test.ts b/services/platform/app/features/contacts/hooks/mutation-hooks.test.ts index 9d2b95a342..898d82bd80 100644 --- a/services/platform/app/features/contacts/hooks/mutation-hooks.test.ts +++ b/services/platform/app/features/contacts/hooks/mutation-hooks.test.ts @@ -17,22 +17,6 @@ vi.mock('@/app/hooks/use-backend-mutation', () => ({ useBackendMutation: () => mockMutationResult, })); -vi.mock('@/convex/_generated/api', () => ({ - api: { - contacts: { - mutations: { - bulkCreateContacts: 'bulkCreateContacts', - createContact: 'createContact', - deleteContact: 'deleteContact', - updateContact: 'updateContact', - }, - queries: { - listContacts: 'listContacts', - }, - }, - }, -})); - import { useBulkCreateContacts, useCreateContact, diff --git a/services/platform/app/features/conversations/hooks/mutation-hooks.test.ts b/services/platform/app/features/conversations/hooks/mutation-hooks.test.ts index c8248d879d..1496d63404 100644 --- a/services/platform/app/features/conversations/hooks/mutation-hooks.test.ts +++ b/services/platform/app/features/conversations/hooks/mutation-hooks.test.ts @@ -17,22 +17,6 @@ vi.mock('@/app/hooks/use-backend-mutation', () => ({ useBackendMutation: () => mockMutationResult, })); -vi.mock('@/convex/_generated/api', () => ({ - api: { - conversations: { - mutations: { - closeConversation: 'closeConversation', - reopenConversation: 'reopenConversation', - markConversationAsRead: 'markConversationAsRead', - markConversationAsSpam: 'markConversationAsSpam', - }, - queries: { - listConversations: 'listConversations', - }, - }, - }, -})); - import { useCloseConversation, useReopenConversation, diff --git a/services/platform/app/features/documents/components/breadcrumb-navigation.test.tsx b/services/platform/app/features/documents/components/breadcrumb-navigation.test.tsx index e83f88f684..9bfbf84bfd 100644 --- a/services/platform/app/features/documents/components/breadcrumb-navigation.test.tsx +++ b/services/platform/app/features/documents/components/breadcrumb-navigation.test.tsx @@ -29,10 +29,6 @@ vi.mock('@/app/hooks/use-organization-id', () => ({ useOrganizationId: () => 'org-1', })); -vi.mock('@/convex/lib/type_cast_helpers', () => ({ - toId: (id: string) => id, -})); - vi.mock('@/app/hooks/use-backend-query', () => ({ useBackendQuery: () => ({ data: [ @@ -43,16 +39,6 @@ vi.mock('@/app/hooks/use-backend-query', () => ({ }), })); -vi.mock('@/convex/_generated/api', () => ({ - api: { - folders: { - queries: { - getFolderBreadcrumb: 'getFolderBreadcrumb', - }, - }, - }, -})); - import { BreadcrumbNavigation } from './breadcrumb-navigation'; describe('BreadcrumbNavigation', () => { diff --git a/services/platform/app/features/documents/components/create-folder-dialog.test.tsx b/services/platform/app/features/documents/components/create-folder-dialog.test.tsx index 977aa72b01..f69bbe1e69 100644 --- a/services/platform/app/features/documents/components/create-folder-dialog.test.tsx +++ b/services/platform/app/features/documents/components/create-folder-dialog.test.tsx @@ -36,10 +36,6 @@ vi.mock('@/app/hooks/use-toast', () => ({ useToast: () => ({ toast: mockToast }), })); -vi.mock('@/convex/lib/type_cast_helpers', () => ({ - toId: (id: string) => id, -})); - const mockTeams = [ { id: 'team-1', name: 'Sales' }, { id: 'team-2', name: 'Support' }, diff --git a/services/platform/app/features/documents/components/document-row-actions.test.tsx b/services/platform/app/features/documents/components/document-row-actions.test.tsx index 2123bbc3ae..a21ebf8d72 100644 --- a/services/platform/app/features/documents/components/document-row-actions.test.tsx +++ b/services/platform/app/features/documents/components/document-row-actions.test.tsx @@ -43,10 +43,6 @@ vi.mock('@/app/hooks/use-toast', () => ({ toast: vi.fn(), })); -vi.mock('@/convex/lib/type_cast_helpers', () => ({ - toId: (id: string) => id, -})); - vi.mock('../hooks/actions', () => ({ useRetryRagIndexing: () => ({ mutateAsync: vi.fn(), isPending: false }), })); diff --git a/services/platform/app/features/documents/components/document-team-tags-dialog.test.tsx b/services/platform/app/features/documents/components/document-team-tags-dialog.test.tsx index 18d6c99601..2e13aa12cd 100644 --- a/services/platform/app/features/documents/components/document-team-tags-dialog.test.tsx +++ b/services/platform/app/features/documents/components/document-team-tags-dialog.test.tsx @@ -59,10 +59,6 @@ vi.mock('@/app/features/settings/teams/hooks/queries', () => ({ useTeams: () => mockTeamsData, })); -vi.mock('@/convex/lib/type_cast_helpers', () => ({ - toId: (id: string) => id, -})); - // Lightweight stand-in for the real multi-select: one checkbox per team that // toggles membership in the selected set, plus an org-wide indicator when the // selection is empty. diff --git a/services/platform/app/features/documents/components/rag-status-badge.test.tsx b/services/platform/app/features/documents/components/rag-status-badge.test.tsx index 0ad64d254b..e8676bc1d4 100644 --- a/services/platform/app/features/documents/components/rag-status-badge.test.tsx +++ b/services/platform/app/features/documents/components/rag-status-badge.test.tsx @@ -59,10 +59,6 @@ vi.mock('@/app/hooks/use-format-date', () => ({ }), })); -vi.mock('@/convex/lib/type_cast_helpers', () => ({ - toId: (id: string) => id, -})); - vi.mock('../hooks/actions', () => ({ useRetryRagIndexing: () => ({ mutateAsync: vi.fn(), isPending: false }), })); diff --git a/services/platform/app/features/documents/hooks/mutation-hooks.test.ts b/services/platform/app/features/documents/hooks/mutation-hooks.test.ts index 0bcc45f080..b1f7342e6d 100644 --- a/services/platform/app/features/documents/hooks/mutation-hooks.test.ts +++ b/services/platform/app/features/documents/hooks/mutation-hooks.test.ts @@ -17,20 +17,6 @@ vi.mock('@/app/hooks/use-backend-mutation', () => ({ useBackendMutation: () => mockMutationResult, })); -vi.mock('@/convex/_generated/api', () => ({ - api: { - documents: { - mutations: { - deleteDocument: 'deleteDocument', - updateDocument: 'updateDocument', - }, - queries: { - listDocuments: 'listDocuments', - }, - }, - }, -})); - import { useDeleteDocument, useUpdateDocument } from './mutations'; describe('useDeleteDocument', () => { diff --git a/services/platform/app/features/documents/hooks/upload-with-progress.test.ts b/services/platform/app/features/documents/hooks/upload-with-progress.test.ts index d44a84acff..1f79ff5e02 100644 --- a/services/platform/app/features/documents/hooks/upload-with-progress.test.ts +++ b/services/platform/app/features/documents/hooks/upload-with-progress.test.ts @@ -8,8 +8,6 @@ vi.mock('@/app/hooks/use-backend-mutation', () => ({ vi.mock('@/app/hooks/use-backend-action', () => ({ useBackendAction: () => ({ mutateAsync: vi.fn() }), })); -vi.mock('@/convex/_generated/api', () => ({ api: {} })); - import { uploadWithProgress } from './mutations'; /** diff --git a/services/platform/app/features/organization/components/onboarding/steps/workspace-step.test.tsx b/services/platform/app/features/organization/components/onboarding/steps/workspace-step.test.tsx index f05954b423..853170c1e8 100644 --- a/services/platform/app/features/organization/components/onboarding/steps/workspace-step.test.tsx +++ b/services/platform/app/features/organization/components/onboarding/steps/workspace-step.test.tsx @@ -13,11 +13,6 @@ vi.mock('@/app/hooks/use-session-user', () => ({ useAuth: () => ({ user: { userId: 'user-1' } }), })); -const recordOrgSwitch = vi.fn().mockResolvedValue(null); -vi.mock('convex/react', () => ({ - useMutation: () => recordOrgSwitch, -})); - vi.mock('@/app/hooks/use-toast', () => ({ toast: vi.fn(), })); diff --git a/services/platform/app/features/settings/organization/hooks/mutation-hooks.test.ts b/services/platform/app/features/settings/organization/hooks/mutation-hooks.test.ts index 03ae705bc3..4d3a0b43a8 100644 --- a/services/platform/app/features/settings/organization/hooks/mutation-hooks.test.ts +++ b/services/platform/app/features/settings/organization/hooks/mutation-hooks.test.ts @@ -17,27 +17,6 @@ vi.mock('@/app/hooks/use-backend-mutation', () => ({ useBackendMutation: () => mockMutationResult, })); -vi.mock('@/convex/_generated/api', () => ({ - api: { - users: { - mutations: { - setMemberPassword: 'setMemberPassword', - createMember: 'createMember', - }, - }, - members: { - mutations: { - removeMember: 'removeMember', - updateMemberRole: 'updateMemberRole', - updateMemberDisplayName: 'updateMemberDisplayName', - }, - queries: { - listByOrganization: 'listByOrganization', - }, - }, - }, -})); - import { useCreateMember, useRemoveMember, diff --git a/services/platform/app/features/settings/teams/hooks/mutation-hooks.test.ts b/services/platform/app/features/settings/teams/hooks/mutation-hooks.test.ts index d3bab9cd88..baf55a0e9d 100644 --- a/services/platform/app/features/settings/teams/hooks/mutation-hooks.test.ts +++ b/services/platform/app/features/settings/teams/hooks/mutation-hooks.test.ts @@ -15,20 +15,6 @@ vi.mock('@/app/hooks/use-backend-mutation', () => ({ }), })); -vi.mock('@/convex/_generated/api', () => ({ - api: { - team_members: { - mutations: { - addMember: 'addMember', - removeMember: 'removeMember', - }, - queries: { - listByTeam: 'listByTeam', - }, - }, - }, -})); - import { useAddTeamMember, useCreateTeamMember, diff --git a/services/platform/app/features/shared/markdown/code-block.test.tsx b/services/platform/app/features/shared/markdown/code-block.test.tsx index d78b4b1a5b..82976ad0e8 100644 --- a/services/platform/app/features/shared/markdown/code-block.test.tsx +++ b/services/platform/app/features/shared/markdown/code-block.test.tsx @@ -5,10 +5,6 @@ import { checkAccessibility } from '@/tests/utils/a11y'; import { CodeBlock, HighlightedCode } from './code-block'; -vi.mock('convex/react', () => ({ - useMutation: () => vi.fn(), -})); - vi.mock('@/app/hooks/use-toast', () => ({ useToast: () => ({ toast: vi.fn() }), })); diff --git a/services/platform/app/hooks/use-auth-from-better-auth.test.tsx b/services/platform/app/hooks/use-auth-from-better-auth.test.tsx deleted file mode 100644 index 1a50d98a74..0000000000 --- a/services/platform/app/hooks/use-auth-from-better-auth.test.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { act, renderHook } from '@testing-library/react'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -const h = vi.hoisted(() => ({ - token: vi.fn(), - // Mutable session state the useSession mock reads — a STABLE object whose - // fields tests mutate before rerendering, mirroring Better Auth's atom. - session: { - data: null as { session: { id: string }; user: { id: string } } | null, - isPending: true, - }, -})); - -vi.mock('@/lib/auth-client', () => ({ - authClient: { - useSession: () => h.session, - convex: { token: h.token }, - }, -})); - -import { - cacheConvexToken, - takeWarmConvexToken, - warmConvexToken, -} from '@/app/lib/auth/convex-token-cache'; -import { - getColdLoadTrace, - resetColdLoadTraceForTests, -} from '@/app/lib/perf/cold-load-trace'; - -import { useAuthFromBetterAuth } from './use-auth-from-better-auth'; - -/** Unsigned JWT with the given payload — the cache never verifies signatures. */ -function makeJwt(claims: Record): string { - const enc = (obj: unknown) => - btoa(JSON.stringify(obj)) - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/, ''); - return `${enc({ alg: 'RS256' })}.${enc(claims)}.sig`; -} - -const inOneHour = () => Math.floor(Date.now() / 1000) + 3600; - -const seedCachedToken = (sessionId: string, userId: string) => { - const token = makeJwt({ sessionId, sub: userId, exp: inOneHour() }); - cacheConvexToken(token); - return token; -}; - -const resolveSession = (sessionId: string, userId: string) => { - h.session.data = { session: { id: sessionId }, user: { id: userId } }; - h.session.isPending = false; -}; - -beforeEach(() => { - vi.clearAllMocks(); - window.sessionStorage.clear(); - void takeWarmConvexToken(); // drain module-level warm state between tests - resetColdLoadTraceForTests(); - h.session.data = null; - h.session.isPending = true; - vi.spyOn(console, 'info').mockImplementation(() => {}); - vi.spyOn(console, 'warn').mockImplementation(() => {}); -}); - -afterEach(() => { - vi.restoreAllMocks(); -}); - -describe('useAuthFromBetterAuth — warm reload (persisted token)', () => { - it('pre-authenticates from the cached token with ZERO HTTP hops', async () => { - const token = seedCachedToken('s1', 'u1'); - - const { result } = renderHook(() => useAuthFromBetterAuth()); - - // Authenticated at mount — the WS handshake can start immediately instead - // of waiting for session HTTP → token HTTP. - expect(result.current.isLoading).toBe(false); - expect(result.current.isAuthenticated).toBe(true); - await expect(result.current.fetchAccessToken()).resolves.toBe(token); - expect(h.token).not.toHaveBeenCalled(); - - // The pre-auth path is visible on the recorded cold-load timeline (AC3). - expect(getColdLoadTrace().map((m) => m.label)).toContain('convex-preauth'); - }); - - it('keeps fetchAccessToken stable when the session resolves to the SAME session (no auth flap)', () => { - seedCachedToken('s1', 'u1'); - const { result, rerender } = renderHook(() => useAuthFromBetterAuth()); - const initialFetch = result.current.fetchAccessToken; - - act(() => resolveSession('s1', 'u1')); - rerender(); - - // Same identity → same callback → ConvexProviderWithAuth never re-runs - // setAuth, so the dashboard subtree is not unmounted mid-load. - expect(result.current.fetchAccessToken).toBe(initialFetch); - expect(result.current.isAuthenticated).toBe(true); - }); - - it('never replays the cached token once the live session turns out to be a DIFFERENT one', async () => { - const staleToken = seedCachedToken('s1', 'u1'); - const freshToken = makeJwt({ - sessionId: 's2', - sub: 'u2', - exp: inOneHour(), - }); - h.token.mockResolvedValue({ data: { token: freshToken } }); - - const { result, rerender } = renderHook(() => useAuthFromBetterAuth()); - const initialFetch = result.current.fetchAccessToken; - - act(() => resolveSession('s2', 'u2')); - rerender(); - - // Mismatch rebuilds the callback (→ setAuth re-runs) and mints for the - // true cookie identity instead of returning the stale cached token. - expect(result.current.fetchAccessToken).not.toBe(initialFetch); - let fetched: string | null = null; - await act(async () => { - fetched = await result.current.fetchAccessToken(); - }); - expect(fetched).toBe(freshToken); - expect(fetched).not.toBe(staleToken); - expect(h.token).toHaveBeenCalledTimes(1); - }); - - it('drops the cached token when Better Auth definitively resolves signed-out', () => { - seedCachedToken('s1', 'u1'); - const { result, rerender } = renderHook(() => useAuthFromBetterAuth()); - expect(result.current.isAuthenticated).toBe(true); - - act(() => { - h.session.data = null; - h.session.isPending = false; - }); - rerender(); - - expect(result.current.isAuthenticated).toBe(false); - expect(window.sessionStorage.getItem('tale:convex-token')).toBeNull(); - }); -}); - -describe('useAuthFromBetterAuth — cold path (no persisted token)', () => { - it('behaves as today: loading until the session resolves, never pre-authenticated', () => { - const { result } = renderHook(() => useAuthFromBetterAuth()); - - expect(result.current.isLoading).toBe(true); - expect(result.current.isAuthenticated).toBe(false); - expect(getColdLoadTrace().map((m) => m.label)).not.toContain( - 'convex-preauth', - ); - }); - - it('consumes the module-load warm mint instead of starting a third HTTP hop', async () => { - const token = makeJwt({ sessionId: 's1', sub: 'u1', exp: inOneHour() }); - h.token.mockResolvedValue({ data: { token } }); - - // router.tsx kicks this at module load, in parallel with the session fetch. - warmConvexToken(); - expect(h.token).toHaveBeenCalledTimes(1); - - // The session (fetched in parallel) resolves to the same identity the - // warm mint belongs to — the provider then asks for the token. - act(() => resolveSession('s1', 'u1')); - const { result } = renderHook(() => useAuthFromBetterAuth()); - let fetched: string | null = null; - await act(async () => { - fetched = await result.current.fetchAccessToken(); - }); - - // The provider's first token request reuses the in-flight warm mint — - // still exactly ONE token HTTP hop for the whole cold load. - expect(fetched).toBe(token); - expect(h.token).toHaveBeenCalledTimes(1); - }); - - it('discards a warm mint from before the sign-in and mints fresh for the new session', async () => { - // A signed-out /log-in load warms the token path too: the mint resolves - // null. That stale result must never stand in for the NEW session's token - // after the user signs in — it would hand the websocket a null token and - // strand auth until the recovery reload. - h.token.mockResolvedValueOnce({ data: null }); - warmConvexToken(); - - const freshToken = makeJwt({ - sessionId: 's1', - sub: 'u1', - exp: inOneHour(), - }); - h.token.mockResolvedValueOnce({ data: { token: freshToken } }); - act(() => resolveSession('s1', 'u1')); - - const { result } = renderHook(() => useAuthFromBetterAuth()); - let fetched: string | null = null; - await act(async () => { - fetched = await result.current.fetchAccessToken(); - }); - - expect(fetched).toBe(freshToken); - expect(h.token).toHaveBeenCalledTimes(2); - }); - - it('a forced refresh mints anew and persists the fresh token for the next load', async () => { - const token = makeJwt({ sessionId: 's1', sub: 'u1', exp: inOneHour() }); - h.token.mockResolvedValue({ data: { token } }); - act(() => resolveSession('s1', 'u1')); - - const { result } = renderHook(() => useAuthFromBetterAuth()); - let fetched: string | null = null; - await act(async () => { - fetched = await result.current.fetchAccessToken({ - forceRefreshToken: true, - }); - }); - - expect(fetched).toBe(token); - expect(h.token).toHaveBeenCalledTimes(1); - // Persisted → the NEXT cold load can pre-authenticate. - expect(window.sessionStorage.getItem('tale:convex-token')).toBe(token); - }); -}); diff --git a/services/platform/app/hooks/use-auth-from-better-auth.ts b/services/platform/app/hooks/use-auth-from-better-auth.ts deleted file mode 100644 index 82675eb724..0000000000 --- a/services/platform/app/hooks/use-auth-from-better-auth.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; - -import { - clearConvexTokenCache, - fetchFreshConvexToken, - isTokenUsable, - readCachedConvexToken, - takeWarmConvexToken, - type CachedConvexToken, -} from '@/app/lib/auth/convex-token-cache'; -import { markColdLoad } from '@/app/lib/perf/cold-load-trace'; -import { authClient } from '@/lib/auth-client'; - -interface UseAuthArgs { - forceRefreshToken?: boolean; -} - -export interface ConvexAuthState { - isLoading: boolean; - isAuthenticated: boolean; - fetchAccessToken: (args?: UseAuthArgs) => Promise; -} - -/** - * The `useAuth` bridge between Better Auth and convex/react's - * `ConvexProviderWithAuth`, replacing `ConvexBetterAuthProvider`'s internal - * `useUseAuthFromBetterAuth` to close the cold-load auth handshake (#2386). - * A deliberate fork — the library hook is not exported, and neither of its - * shapes fits: - * - * - Without `initialToken` it serializes the hops: session HTTP → token HTTP → - * WS authenticate (~830ms of blocked auth-gated queries in the epic's trace). - * - With `initialToken` it rebuilds `fetchAccessToken` when the session - * resolves (keyed on session id, unknown at mount), which re-runs `setAuth` - * and flaps `useSessionUser().isAuthenticated` — unmounting the whole - * dashboard subtree mid-load. - * - * This hook instead: - * - * 1. Pre-authenticates the WS with the persisted last-known token - * (`convex-token-cache`) — reporting `isAuthenticated` immediately so - * `setAuth` runs at mount; the Convex auth manager confirms the cached - * token and immediately force-refreshes a fresh cookie-minted one. - * 2. Keys `fetchAccessToken` on the token's OWN `sessionId` claim until the - * live session resolves. On the common warm reload both match, so the - * callback identity is stable and no flap occurs; a mismatch (a different - * session/user owns the cookie) rebuilds it, which re-runs `setAuth` with a - * freshly minted token for the true cookie identity. - * 3. With no persisted token, behaves as today (wait for the session), except - * the first `fetchAccessToken` consumes the token fetch already in flight - * since module load (`warmConvexToken`) — session and token HTTP hops run - * in parallel instead of serially. - * - * Auth-gated queries still unlock only when the Convex BACKEND confirms a - * token (`useSessionUser().isAuthenticated` — see `ConvexProviderWithAuth`); - * this hook never fabricates that confirmation. - */ -export function useAuthFromBetterAuth(): ConvexAuthState { - const { data: session, isPending: isSessionPending } = - authClient.useSession(); - const liveSessionId = session?.session?.id ?? null; - - const [cached, setCached] = useState( - readCachedConvexToken, - ); - const cachedRef = useRef(cached); - cachedRef.current = cached; - - // Trace the pre-auth path so the [cold-load] timeline shows when a persisted - // token skipped the serial handshake (dedup'd; effect keeps render pure). - useEffect(() => { - if (cachedRef.current) markColdLoad('convex-preauth'); - }, []); - - // Better Auth definitively resolved signed-out → drop the cached token so - // `isAuthenticated` flips false and the provider clears the WS auth. - useEffect(() => { - if (!session && !isSessionPending && cachedRef.current) { - clearConvexTokenCache(); - setCached(null); - } - }, [session, isSessionPending]); - - // The session identity the WS auth is bound to: the live session once known, - // else the persisted token's own session claim. Changing it rebuilds - // `fetchAccessToken`, which makes `ConvexProviderWithAuth` re-run `setAuth`. - const authSessionId = liveSessionId ?? cached?.sessionId ?? null; - - const pendingRef = useRef | null>(null); - - const fetchAccessToken = useCallback( - async ({ forceRefreshToken = false }: UseAuthArgs = {}) => { - if (!forceRefreshToken) { - // Pre-auth fast path: hand the WS the last-known token synchronously — - // but only when it belongs to the session this callback is keyed to; a - // stale record for another session is never replayed. - const current = cachedRef.current; - if ( - current && - current.sessionId === authSessionId && - isTokenUsable(current) - ) { - return current.token; - } - if (pendingRef.current) return pendingRef.current; - } - - // Consume the mint in flight since module load if it's still unclaimed - // (`warmConvexToken`) — when it minted a token for the session this - // callback is keyed to it IS a fresh cookie mint of the true current - // identity, so it satisfies forced refreshes too. A warm result that - // resolved signed-out or for another session (e.g. minted on the login - // screen BEFORE this sign-in) is discarded — handing the websocket that - // stale null/token would strand auth — and a fresh token minted instead. - const consumeWarmOrMint = async (): Promise => { - const warm = takeWarmConvexToken(); - if (warm) { - const record = await warm; - if ( - record && - (authSessionId === null || record.sessionId === authSessionId) - ) { - return record; - } - } - return fetchFreshConvexToken(); - }; - const minted = consumeWarmOrMint() - .then((record) => { - setCached(record); - return record?.token ?? null; - }) - .catch((error: unknown) => { - console.warn('Failed to fetch Convex token:', error); - setCached(null); - return null; - }) - .finally(() => { - pendingRef.current = null; - }); - pendingRef.current = minted; - return minted; - }, - [authSessionId], - ); - - // Usable = a cached token that doesn't contradict the resolved session. - const cachedMatchesSession = - cached !== null && - (liveSessionId === null || cached.sessionId === liveSessionId); - - return useMemo( - () => ({ - isLoading: isSessionPending && !cachedMatchesSession, - isAuthenticated: Boolean(session?.session) || cachedMatchesSession, - fetchAccessToken, - }), - [isSessionPending, cachedMatchesSession, session, fetchAccessToken], - ); -} diff --git a/services/platform/app/hooks/use-session-idle-watchdog.test.tsx b/services/platform/app/hooks/use-session-idle-watchdog.test.tsx index 09d99c1006..fea7034322 100644 --- a/services/platform/app/hooks/use-session-idle-watchdog.test.tsx +++ b/services/platform/app/hooks/use-session-idle-watchdog.test.tsx @@ -40,10 +40,6 @@ vi.mock('@/app/hooks/use-backend-query', () => ({ data: args === 'skip' ? undefined : h.state.policyRow, }), })); -vi.mock('@/convex/_generated/api', () => ({ - api: { governance: { queries: { getPolicy: 'getPolicy' } } }, -})); - import { useSessionIdleWatchdog } from './use-session-idle-watchdog'; const MINUTE = 60_000; diff --git a/services/platform/app/hooks/use-session-user.ts b/services/platform/app/hooks/use-session-user.ts index 7fb658d18b..61757cbef1 100644 --- a/services/platform/app/hooks/use-session-user.ts +++ b/services/platform/app/hooks/use-session-user.ts @@ -1,6 +1,5 @@ import { useQuery } from '@tanstack/react-query'; -import { clearConvexTokenCache } from '@/app/lib/auth/convex-token-cache'; import { currentUserQuery } from '@/app/lib/backend/account'; import { clearMemberContextCache } from '@/app/lib/member-context-cache'; import { clearTitleSuffix } from '@/app/lib/title-suffix'; @@ -20,10 +19,9 @@ function useConvexAuthUser() { // than the previous org's suffix (the sign-out flows hard-navigate, so the // next document title is composed from a fresh, empty cache). clearTitleSuffix(); - // Sign-out revoked the session server-side: drop the pre-auth caches so - // the next load can't attempt a websocket pre-authentication with the - // revoked token or hydrate the shell for the signed-out account (#2386). - clearConvexTokenCache(); + // Sign-out revoked the session server-side: drop the pre-auth cache so + // the next load can't hydrate the shell for the signed-out account + // (#2386). clearMemberContextCache(); }; diff --git a/services/platform/app/lib/auth/convex-token-cache.test.ts b/services/platform/app/lib/auth/convex-token-cache.test.ts deleted file mode 100644 index 0e70240cc8..0000000000 --- a/services/platform/app/lib/auth/convex-token-cache.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -// @vitest-environment jsdom -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -const h = vi.hoisted(() => ({ - token: vi.fn(), -})); - -vi.mock('@/lib/auth-client', () => ({ - authClient: { convex: { token: h.token } }, -})); - -import { - cacheConvexToken, - clearConvexTokenCache, - fetchFreshConvexToken, - getCachedConvexTokenUserId, - isTokenUsable, - readCachedConvexToken, - takeWarmConvexToken, - warmConvexToken, -} from './convex-token-cache'; - -const STORAGE_KEY = 'tale:convex-token'; - -/** Unsigned JWT with the given payload — the cache never verifies signatures. */ -function makeJwt(claims: Record): string { - const enc = (obj: unknown) => - btoa(JSON.stringify(obj)) - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/, ''); - return `${enc({ alg: 'RS256' })}.${enc(claims)}.sig`; -} - -const inOneHour = () => Math.floor(Date.now() / 1000) + 3600; - -beforeEach(() => { - vi.clearAllMocks(); - window.sessionStorage.clear(); - // Drain any warm mint left over from a previous test (module-level state). - void takeWarmConvexToken(); - vi.spyOn(console, 'warn').mockImplementation(() => {}); -}); - -afterEach(() => { - vi.restoreAllMocks(); -}); - -describe('cacheConvexToken / readCachedConvexToken', () => { - it('round-trips a valid token with its decoded claims', () => { - const token = makeJwt({ sessionId: 's1', sub: 'u1', exp: inOneHour() }); - const cached = cacheConvexToken(token); - - expect(cached).toMatchObject({ token, sessionId: 's1', userId: 'u1' }); - expect(readCachedConvexToken()).toEqual(cached); - expect(getCachedConvexTokenUserId()).toBe('u1'); - }); - - it('rejects an expired token and removes it (cold path restored)', () => { - const token = makeJwt({ - sessionId: 's1', - sub: 'u1', - exp: Math.floor(Date.now() / 1000) - 10, - }); - cacheConvexToken(token); - - expect(readCachedConvexToken()).toBeNull(); - expect(window.sessionStorage.getItem(STORAGE_KEY)).toBeNull(); - }); - - it('rejects a token expiring within the pre-auth leeway window', () => { - const record = cacheConvexToken( - makeJwt({ - sessionId: 's1', - sub: 'u1', - // 10s from now — inside the 30s leeway: too close to survive the - // WS handshake. - exp: Math.floor(Date.now() / 1000) + 10, - }), - ); - - expect(record && isTokenUsable(record)).toBe(false); - expect(readCachedConvexToken()).toBeNull(); - }); - - it('rejects tokens with missing or malformed claims', () => { - expect(cacheConvexToken(makeJwt({ sub: 'u1', exp: inOneHour() }))).toBe( - null, - ); - expect(cacheConvexToken('not-a-jwt')).toBeNull(); - - // A malformed value that somehow reached storage is discarded on read. - window.sessionStorage.setItem(STORAGE_KEY, 'garbage'); - expect(readCachedConvexToken()).toBeNull(); - expect(window.sessionStorage.getItem(STORAGE_KEY)).toBeNull(); - }); - - it('clearConvexTokenCache removes the persisted token', () => { - cacheConvexToken(makeJwt({ sessionId: 's1', sub: 'u1', exp: inOneHour() })); - clearConvexTokenCache(); - - expect(window.sessionStorage.getItem(STORAGE_KEY)).toBeNull(); - expect(getCachedConvexTokenUserId()).toBeUndefined(); - }); -}); - -describe('fetchFreshConvexToken', () => { - it('persists a freshly minted token', async () => { - const token = makeJwt({ sessionId: 's2', sub: 'u2', exp: inOneHour() }); - h.token.mockResolvedValueOnce({ data: { token } }); - - const record = await fetchFreshConvexToken(); - - expect(record?.sessionId).toBe('s2'); - expect(readCachedConvexToken()?.token).toBe(token); - }); - - it('clears the persisted copy when the backend refuses to mint', async () => { - cacheConvexToken(makeJwt({ sessionId: 's1', sub: 'u1', exp: inOneHour() })); - h.token.mockResolvedValueOnce({ data: null }); - - await expect(fetchFreshConvexToken()).resolves.toBeNull(); - expect(window.sessionStorage.getItem(STORAGE_KEY)).toBeNull(); - }); -}); - -describe('warmConvexToken / takeWarmConvexToken', () => { - it('mints once (single-flight) and hands the promise to exactly one consumer', async () => { - const token = makeJwt({ sessionId: 's3', sub: 'u3', exp: inOneHour() }); - h.token.mockResolvedValue({ data: { token } }); - - warmConvexToken(); - warmConvexToken(); - expect(h.token).toHaveBeenCalledTimes(1); - - const warm = takeWarmConvexToken(); - expect(warm).not.toBeNull(); - await expect(warm).resolves.toMatchObject({ token, userId: 'u3' }); - - // One-shot: a second consumer must mint anew, never reuse a token that - // could belong to a previous sign-in. - expect(takeWarmConvexToken()).toBeNull(); - }); -}); diff --git a/services/platform/app/lib/auth/convex-token-cache.ts b/services/platform/app/lib/auth/convex-token-cache.ts deleted file mode 100644 index 227413e030..0000000000 --- a/services/platform/app/lib/auth/convex-token-cache.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { authClient } from '@/lib/auth-client'; -import { isRecord } from '@/lib/utils/type-utils'; - -/** - * Last-known Convex JWT, persisted so the NEXT cold load can pre-authenticate - * the WebSocket while the Better Auth session revalidates in parallel — this - * collapses the serial cold-load auth handshake (session HTTP → token HTTP → - * WS authenticate) that blocks every auth-gated query (epic #2386). - * - * Safety model (correctness over speed): - * - The record stores the JWT's own `exp`, `sessionId`, and `sub` claims; - * reads reject expired (with leeway) or malformed records — a stale cache - * degrades to today's serial path, never to a broken one. - * - `sessionStorage`, not `localStorage`: the token is a bearer credential, so - * it stays scoped to the tab and dies with it. The epic's measured scenario - * (hard refresh of a dashboard tab) is exactly what survives. - * - The Convex client treats a cached token as provisional: after the server - * confirms it, the auth manager immediately force-refreshes a fresh - * cookie-minted token (see convex's `AuthenticationManager.setConfig`), so a - * cached token is only ever live for ~one round trip. - * - The cache is cleared on sign-out, whenever Better Auth resolves signed-out, - * and when an auth screen mounts (`/_auth` layout) — the only same-tab door - * to a user switch — so one user's token cannot pre-authenticate another's - * load (see `use-auth-from-better-auth.ts` for the session-binding check). - */ - -const STORAGE_KEY = 'tale:convex-token'; - -/** Reject tokens that would expire before the WS handshake can use them. */ -const EXPIRY_LEEWAY_MS = 30 * 1000; - -const isBrowser = typeof window !== 'undefined'; - -export interface CachedConvexToken { - token: string; - /** Better Auth session id the token was minted for (`sessionId` claim). */ - sessionId: string; - /** Better Auth user id the token authenticates (`sub` claim). */ - userId: string; - /** JWT `exp` claim, in milliseconds since epoch. */ - expiresAt: number; -} - -/** - * Decode a JWT payload without verifying the signature — the server (and the - * Convex backend) remain the only verifiers; this is used purely to read the - * token's own claims for client-side cache bookkeeping. - */ -function decodeJwtPayload(token: string): Record | null { - const payload = token.split('.')[1]; - if (!payload) return null; - try { - const base64 = payload.replace(/-/g, '+').replace(/_/g, '/'); - const parsed: unknown = JSON.parse(atob(base64)); - return isRecord(parsed) ? parsed : null; - } catch (error) { - console.warn('Failed to decode cached Convex token payload:', error); - return null; - } -} - -function toRecord(token: string): CachedConvexToken | null { - const claims = decodeJwtPayload(token); - if (!claims) return null; - const { sessionId, sub, exp } = claims; - if ( - typeof sessionId !== 'string' || - typeof sub !== 'string' || - typeof exp !== 'number' - ) { - return null; - } - return { token, sessionId, userId: sub, expiresAt: exp * 1000 }; -} - -export function isTokenUsable(record: CachedConvexToken): boolean { - return record.expiresAt - EXPIRY_LEEWAY_MS > Date.now(); -} - -/** - * The persisted last-known token, or `null` when absent, malformed, or expired - * (expired/malformed records are removed so the cold path stays clean). - */ -export function readCachedConvexToken(): CachedConvexToken | null { - if (!isBrowser) return null; - let token: string | null = null; - try { - token = window.sessionStorage.getItem(STORAGE_KEY); - } catch (error) { - console.warn('Failed to read cached Convex token:', error); - return null; - } - if (!token) return null; - const record = toRecord(token); - if (!record || !isTokenUsable(record)) { - clearConvexTokenCache(); - return null; - } - return record; -} - -/** - * Persist a freshly minted token (or clear with `null`). Returns the decoded - * record, or `null` when the token was absent or malformed. - */ -export function cacheConvexToken( - token: string | null, -): CachedConvexToken | null { - const record = token ? toRecord(token) : null; - if (isBrowser) { - try { - if (record) { - window.sessionStorage.setItem(STORAGE_KEY, record.token); - } else { - window.sessionStorage.removeItem(STORAGE_KEY); - } - } catch (error) { - // Quota / security errors — pre-auth is lost for the next reload only; - // this load already holds the token in memory. - console.warn('Failed to persist Convex token:', error); - } - } - return record; -} - -export function clearConvexTokenCache(): void { - cacheConvexToken(null); -} - -/** - * User id of the persisted token, if a usable one exists. Used to key other - * shell caches to the identity the WS will (pre-)authenticate as, before the - * session query has resolved. - */ -export function getCachedConvexTokenUserId(): string | undefined { - return readCachedConvexToken()?.userId; -} - -/** - * Mint a fresh Convex JWT from the current session cookie and persist it. - * Resolves `null` when signed out or on transport failure (the auth client's - * fetch layer already retries 5xx) — a `null` result clears the persisted copy - * so a later cold load can never pre-authenticate with a token the backend - * just refused to renew. - */ -export async function fetchFreshConvexToken(): Promise { - const result = await authClient.convex?.token?.({ - fetchOptions: { throw: false }, - }); - return cacheConvexToken(result?.data?.token ?? null); -} - -let warmToken: Promise | null = null; - -/** - * Kick the Convex token mint at module load, in PARALLEL with the session - * fetch (`warmSession`), instead of serially after it — and keep the promise - * so the auth provider's first `fetchAccessToken` can consume the in-flight - * result rather than starting a third HTTP hop. - */ -export function warmConvexToken(): void { - if (!isBrowser || warmToken) return; - warmToken = fetchFreshConvexToken().catch((error: unknown) => { - console.warn('Failed to warm Convex token:', error); - return null; - }); -} - -/** - * One-shot: the module-load warm token fetch, or `null` once consumed (or when - * never warmed). Single consumer semantics keep a long-lived page from reusing - * a token minted for a previous sign-in. - */ -export function takeWarmConvexToken(): Promise | null { - const taken = warmToken; - warmToken = null; - return taken; -} diff --git a/services/platform/app/lib/auth/session-query.test.ts b/services/platform/app/lib/auth/session-query.test.ts index f5e3bb8cff..7f7dcc6b75 100644 --- a/services/platform/app/lib/auth/session-query.test.ts +++ b/services/platform/app/lib/auth/session-query.test.ts @@ -2,18 +2,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const h = vi.hoisted(() => ({ - // Never resolves — proves the token warm is kicked in parallel with (not - // serially after) the session fetch. + // Never resolves — warmSession must not await it. getSession: vi.fn(() => new Promise(() => {})), - warmConvexToken: vi.fn(), })); vi.mock('@/lib/auth-client', () => ({ authClient: { getSession: h.getSession }, })); -vi.mock('@/app/lib/auth/convex-token-cache', () => ({ - warmConvexToken: h.warmConvexToken, -})); import { sessionQueryOptions, warmSession } from './session-query'; @@ -22,14 +17,13 @@ describe('warmSession', () => { vi.clearAllMocks(); }); - it('kicks the session fetch and the Convex token warm in parallel', () => { + it('kicks the session fetch without awaiting it', () => { warmSession(); - // Both hops start in the same synchronous tick; the session promise is - // still pending, so the token warm cannot be gated on its result (this is - // the serial→parallel collapse of epic #2386). + // The fetch starts in the same synchronous tick; the pending promise + // proves nothing downstream is gated on its result (the serial→parallel + // collapse of epic #2386). expect(h.getSession).toHaveBeenCalledTimes(1); - expect(h.warmConvexToken).toHaveBeenCalledTimes(1); }); }); diff --git a/services/platform/app/lib/auth/session-query.ts b/services/platform/app/lib/auth/session-query.ts index 310eda5e75..65db5fccfd 100644 --- a/services/platform/app/lib/auth/session-query.ts +++ b/services/platform/app/lib/auth/session-query.ts @@ -1,6 +1,5 @@ import type { QueryClient } from '@tanstack/react-query'; -import { warmConvexToken } from '@/app/lib/auth/convex-token-cache'; import { authClient } from '@/lib/auth-client'; /** @@ -61,10 +60,4 @@ export function invalidateAuthState(queryClient: QueryClient): Promise { export function warmSession(): void { if (typeof window === 'undefined') return; void authClient.getSession(); - // Also mint the Convex JWT at module load, in PARALLEL with the session - // fetch — the second serial hop that gates the websocket authentication on - // cold load. The result is persisted and kept in flight so the auth - // provider's first `fetchAccessToken` consumes it instead of starting a - // third HTTP hop (see convex-token-cache). - warmConvexToken(); } diff --git a/services/platform/app/lib/org-error-recovery.ts b/services/platform/app/lib/org-error-recovery.ts index 27ebcd27ee..1e36447a18 100644 --- a/services/platform/app/lib/org-error-recovery.ts +++ b/services/platform/app/lib/org-error-recovery.ts @@ -16,12 +16,11 @@ * create-org wizard) and re-persists it. * * A cache subscription, NOT `QueryCache({ onError })`: `onError` only fires - * when a queryFn rejects, but the @convex-dev/react-query bridge delivers a - * LIVE subscription failure by writing the error state directly - * (`query.setState`) — exactly what an open tab receives when its org is - * deleted mid-session. Observing cache events covers both delivery paths - * (verified manually: an open dashboard tab whose org was deleted only - * received the structured error via the setState path). + * when a queryFn rejects, but an error can also land by a write to the query + * state directly (`query.setState`) — the path the retired live-subscription + * bridge used when an open tab's org was deleted mid-session. Observing cache + * events covers both delivery paths (verified manually at the time: the open + * dashboard tab only received the structured error via the setState path). * * Deliberately NOT triggered by `ORG_FORBIDDEN` (org exists, caller isn't a * member): the dashboard layout renders the intentional "you've been removed" diff --git a/services/platform/app/routes/_auth.tsx b/services/platform/app/routes/_auth.tsx index 8e6cf6ca49..076923d7ec 100644 --- a/services/platform/app/routes/_auth.tsx +++ b/services/platform/app/routes/_auth.tsx @@ -9,7 +9,6 @@ import { useEffect } from 'react'; import { LogoLink } from '@/app/components/ui/logo/logo-link'; import { AuthSsoHeader } from '@/app/features/auth/components/auth-sso-header'; -import { clearConvexTokenCache } from '@/app/lib/auth/convex-token-cache'; import { sessionQueryOptions } from '@/app/lib/auth/session-query'; import { clearMemberContextCache } from '@/app/lib/member-context-cache'; @@ -35,11 +34,9 @@ function isSsoOrgPickerStep(pathname: string, searchStr: string): boolean { function AuthLayout() { // An auth screen is the only same-tab door to a user switch: drop the - // pre-auth caches so the next sign-in can never pre-authenticate the - // websocket — or hydrate the dashboard shell — as the previous account - // (see convex-token-cache / member-context-cache, epic #2386). + // pre-auth cache so the next sign-in can never hydrate the dashboard + // shell as the previous account (see member-context-cache, epic #2386). useEffect(() => { - clearConvexTokenCache(); clearMemberContextCache(); }, []); diff --git a/services/platform/app/routes/dashboard/$id.tsx b/services/platform/app/routes/dashboard/$id.tsx index 75b5178f0b..bf5fad33fc 100644 --- a/services/platform/app/routes/dashboard/$id.tsx +++ b/services/platform/app/routes/dashboard/$id.tsx @@ -33,7 +33,6 @@ import { useAuth } from '@/app/hooks/use-session-user'; import { TeamFilterProvider } from '@/app/hooks/use-team-filter'; import { toast } from '@/app/hooks/use-toast'; import { setActiveOrganizationId } from '@/app/lib/active-organization'; -import { getCachedConvexTokenUserId } from '@/app/lib/auth/convex-token-cache'; import { sessionQueryOptions } from '@/app/lib/auth/session-query'; import { memberContextQuery, @@ -164,13 +163,12 @@ function DashboardLayout() { memberContext?.status === 'ok' ? memberContext.role : null; // Instant shell hydration (epic #2386): while the live member context is // still resolving on a fresh load, fall back to the persisted last-known - // role — only once the websocket is backend-authenticated (so everything the + // role — only once the session is authenticated (so everything the // hydrated shell mounts fires authorized queries) and only when the cached - // record matches this exact user + org (the read rejects everything else; - // the identity hint is the resolved session's user, or before it resolves, - // the user the pre-auth token authenticated as). The live subscription - // confirms or corrects the shell within one round trip. - const shellUserId = session?.data?.user?.id ?? getCachedConvexTokenUserId(); + // record matches this exact user + org (the read rejects everything else). + // The live membership query confirms or corrects the shell within one + // round trip. + const shellUserId = session?.data?.user?.id ?? null; const persistedRole = isAuthenticated && memberContext === undefined && shellUserId ? readCachedMemberContextRole(shellUserId, organizationId) diff --git a/services/platform/app/routes/dashboard/dashboard-layout.test.tsx b/services/platform/app/routes/dashboard/dashboard-layout.test.tsx index d246315648..9179b0468e 100644 --- a/services/platform/app/routes/dashboard/dashboard-layout.test.tsx +++ b/services/platform/app/routes/dashboard/dashboard-layout.test.tsx @@ -26,7 +26,6 @@ vi.mock('@tanstack/react-router', () => ({ // Route.useRouteContext(); stub it so the prewarm is a harmless no-op here. useRouteContext: () => ({ queryClient: { fetchQuery: vi.fn().mockResolvedValue(undefined) }, - convexQueryClient: { convexClient: { action: vi.fn() } }, }), ...config, }), @@ -45,11 +44,6 @@ const mockUseConvexAuth = vi.fn(() => ({ isLoading: false, isAuthenticated: true, })); -vi.mock('convex/react', () => ({ - useSessionUser: () => mockUseConvexAuth(), - useMutation: () => vi.fn(), -})); - // The layout's auth flags come from the SESSION PROBE now (useAuth), not // the websocket — the same control var drives both in these scenarios. vi.mock('@/app/hooks/use-session-user', () => ({ @@ -99,16 +93,6 @@ vi.mock('@/lib/permissions/ability', () => ({ defineAbilityFor: () => ({ can: () => false, cannot: () => true }), })); -vi.mock('@/convex/_generated/api', () => ({ - api: { - members: { queries: { getCurrentMemberContext: 'mock-query-ref' } }, - two_factor: { queries: { getStatus: 'mock-status-ref' } }, - organizations: { - record_org_switch: { recordOrgSwitch: 'mock-record-org-switch' }, - }, - }, -})); - vi.mock('@/app/features/auth/components/two-factor-grace-banner', () => ({ TwoFactorGraceBanner: () => null, })); diff --git a/services/platform/backend/core/lib/rls/MEMBERSHIP_MIRROR_DESIGN.md b/services/platform/backend/core/lib/rls/MEMBERSHIP_MIRROR_DESIGN.md deleted file mode 100644 index e899c271c3..0000000000 --- a/services/platform/backend/core/lib/rls/MEMBERSHIP_MIRROR_DESIGN.md +++ /dev/null @@ -1,3843 +0,0 @@ -# Membership Mirror — Design & Decision (scoped implementation shipped) - -> Status: SCOPED VERSION IMPLEMENTED. The `member`-table mirror is now live as -> a PERFORMANCE CACHE for the two RLS hot-path readers — `getUserOrganizations` -> and `isOrgMember` — which read `memberMirror` (a local indexed table) and -> fall back to Better Auth on a miss. The authoritative gate -> (`getOrganizationMember`, with its email-fallback and `trustedRole` -> override) is UNCHANGED and still reads Better Auth, per the adversarial -> audit's verdict (Section IX) that the mirror must not be the sole source of -> truth. Team membership (`teamMember`/`getUserTeamIds`) is NOT mirrored. -> -> Files: `members/schema.ts` (table + reconcile cursor), `members/mirror_sync.ts` -> (inline `upsertMemberMirror`/`deleteMemberMirrorByMemberId` + internal -> `resyncOrgMemberMirror`/`cascadeDeleteOrgMembersMirror`), -> `members/mirror_reconciliation.ts` (hourly cron — backfills + repairs drift), -> with inline sync wired into every member write path (members/mutations.ts, -> users/_, sso_providers/_, betterAuth/trusted_headers/\*), the org-create / -> accept-invitation auth hooks, and the auth after-middleware catch-all -> (leave / remove-member / update-member-role / delete). Unit-tested in -> `members/member_mirror.test.ts`; the schema + functions push cleanly to a -> live Convex backend. The accepted residual risk is a bounded -> privilege-retention window (a stale mirror row after a partial write-path -> failure) until the inline delete / after-middleware / hourly reconcile -> converge — see Section IX. Full multi-write-path E2E on a live Better Auth -> deployment (SSO, trusted-headers, invitation, org-delete cascades) remains -> the recommended pre-production validation. -> -> Original researched design follows verbatim. - -## Enumeration of write paths (the coverage surface) - -### AREA: Better Auth `member` table write paths in services/platform/convex - -EXHAUSTIVE ENUMERATION OF ALL MEMBER TABLE WRITERS: - -## 1. Direct Adapter Writes (via components.betterAuth.adapter) - -### Organization Creation (Better Auth Plugin) - -- **File**: services/platform/convex/auth.ts, lines 724-765 -- **Operation**: `adapter.create` (called implicitly by Better Auth org plugin) -- **How**: When client calls `authClient.organization.create()`, Better Auth plugin automatically creates a member row with `role='owner'` for the creator user -- **Hook**: `afterCreateOrganization` hook fires AFTER the member row is persisted (line 724) -- **Details**: "Better Auth has already persisted the member record with role='owner' before invoking this hook" (comment line 746) - -### Invitation Acceptance (Better Auth Plugin) - -- **File**: services/platform/convex/auth.ts, lines 767-790 -- **Operation**: `adapter.create` (called implicitly by Better Auth org plugin) -- **How**: When user accepts an invitation via `authClient.organization.acceptInvitation()`, Better Auth plugin automatically creates a member row with the invited role -- **Hook**: `afterAcceptInvitation` hook fires AFTER the member row is persisted (line 767) -- **Details**: "Better Auth persists the member record before invoking this hook, so `data.member.role` is authoritative" (comment lines 768-770) - -### addMember Mutation - -- **File**: services/platform/convex/members/mutations.ts, lines 37-116 -- **Operation**: `adapter.create` at line 78 -- **How**: `ctx.runMutation(components.betterAuth.adapter.create, { input: { model: 'member', data: { organizationId, userId, role, createdAt } } })` -- **Details**: Requires caller to be admin of the organization; creates member with specified role - -### removeMember Mutation - -- **File**: services/platform/convex/members/mutations.ts, lines 118-220 -- **Operation**: `adapter.deleteOne` at line 189 -- **How**: `ctx.runMutation(components.betterAuth.adapter.deleteOne, { input: { model: 'member', where: [{ field: '_id', value: memberId, operator: 'eq' }] } })` -- **Details**: Requires caller to be admin; prevents removal of owner; triggers cascadeOnMemberRemoved for personalization cleanup (line 197) - -### updateMemberRole Mutation - -- **File**: services/platform/convex/members/mutations.ts, lines 222-368 -- **Operations**: `adapter.updateMany` at lines 338 and 425 -- **How Line 338**: Updates target member's role: `ctx.runMutation(components.betterAuth.adapter.updateMany, { input: { model: 'member', where: [{ field: '_id', value: memberId, operator: 'eq' }], update: { role: newRole } } })` -- **How Line 425**: Demotes previous owner from owner to admin during transfer -- **Details**: Prevents owner role change, requires admin/owner caller; ensures org always has at least one admin - -### transferOwnership Mutation - -- **File**: services/platform/convex/members/mutations.ts, lines 370-467 -- **Operations**: `adapter.updateMany` at lines 415 and 425 -- **How Line 415**: Promotes target member to owner -- **How Line 425**: Demotes caller from owner to admin -- **Details**: Only owners can call; atomically swaps owner role - -### addMember (SSO Path) - -- **File**: services/platform/convex/sso_providers/find_or_create_sso_user.ts, lines 25-188 -- **Operations**: `adapter.create` for member at lines 118 and 175 -- **How Line 118**: For existing user being added to org: `ctx.runMutation(components.betterAuth.adapter.create, { input: { model: 'member', data: { organizationId, userId: existingUserId, role, createdAt } } })` -- **How Line 175**: For newly created SSO user: `ctx.runMutation(components.betterAuth.adapter.create, { input: { model: 'member', data: { organizationId, userId, role, createdAt } } })` -- **Details**: Called by SSO provider endpoints to auto-provision users; checks for existing membership before creation - -### addMember (Trusted Headers Path) - -- **File**: services/platform/convex/betterAuth/trusted_headers/find_or_create_user_from_headers.ts, lines 34-239 -- **Operations**: `adapter.create` for member at lines 162, 207 -- **How Line 162**: For existing org, attach new trusted-headers user: `ctx.runMutation(components.betterAuth.adapter.create, { input: { model: 'member', data: { organizationId: existingOrgId, userId, role: 'member', createdAt } } })` -- **How Line 207**: For first trusted-headers user, create default org and make them admin: `ctx.runMutation(components.betterAuth.adapter.create, { input: { model: 'member', data: { organizationId: newOrgId, userId, role: 'admin', createdAt } } })` -- **Details**: Used in trusted-header SSO mode; role is placeholder ('member' or 'admin'); actual role comes from session/JWT (lines 4-11 comment) - -### addMemberInternal - -- **File**: services/platform/convex/users/add_member_internal.ts, lines 25-48 -- **Operation**: `adapter.create` at line 30 -- **How**: `ctx.runMutation(components.betterAuth.adapter.create, { input: { model: 'member', data: { organizationId, userId: identityId, role, createdAt } } })` -- **Details**: Internal helper used to avoid circular dependencies; no RLS checks (line 29) - -### createMember (for Admin-Created Users) - -- **File**: services/platform/convex/users/create_member.ts, lines 40-231 -- **Operations**: `adapter.create` at lines 126 and 205 -- **How Line 126**: Existing user being added to org: `ctx.runMutation(components.betterAuth.adapter.create, { input: { model: 'member', data: { organizationId, userId: existingUserId, role, createdAt } } })` -- **How Line 205**: Newly created user being added to org: `ctx.runMutation(components.betterAuth.adapter.create, { input: { model: 'member', data: { organizationId, userId: betterAuthUserId, role, createdAt } } })` -- **Details**: Called by admin add-member flow; requires admin/owner role; user signup happens before member creation - -### createUserWithoutSession - -- **File**: services/platform/convex/users/create_user_without_session.ts, lines 37-136 -- **Operation**: `adapter.create` at line 115 -- **How**: `ctx.runMutation(components.betterAuth.adapter.create, { input: { model: 'member', data: { organizationId, userId: betterAuthUserId, role, createdAt } } })` -- **Details**: Similar to createMember but doesn't create a session for the admin; used for programmatic user creation - -### Organization Deletion (Better Auth Plugin) - -- **File**: services/platform/convex/organizations/delete_cleanup.ts & auth.ts -- **Operation**: `adapter.deleteMany` (called implicitly by Better Auth org plugin via client) -- **How**: When client calls `authClient.organization.delete()`, Better Auth plugin automatically deletes all member rows where organizationId matches -- **Details**: Client-side operation via authClient; server-side delete_cleanup is a pre-deletion audit/cascade helper (line 79-80 comment: "cascadeOnOrgDeleted hard-deletes userMemories + userPreferences"). Member deletion happens when org deletion is committed (lines 6 & 84 comments) - -## 2. Member Table Updates via Migrations - -### migrate_org_creators Migration - -- **File**: services/platform/convex/migrations/migrate_org_creators.ts, lines 26-132 -- **Operation**: `adapter.updateMany` at line 89 -- **How**: `ctx.runMutation(components.betterAuth.adapter.updateMany, { input: { model: 'member', where: [{ field: '_id', value: creator._id, operator: 'eq' }], update: { role: 'owner' } } })` -- **Details**: Idempotent migration to set earliest member of each org to 'owner' role; skips orgs that already have an owner - -## 3. Non-Member Operations (Verified NOT Member Writers) - -These files have 'member' in them but do NOT write to the member table: - -- `services/platform/convex/two_factor/mutations.ts` - only reads member; does NOT write -- `services/platform/convex/two_factor/internal_mutations.ts` - only reads member; does NOT write -- `services/platform/convex/team_members/mutations.ts` - writes to `teamMember` model, NOT `member` model -- All query/read files (members/queries.ts, etc.) - read only - -## Verification of Prior Analysis Claims - -CONFIRMED: - -1. ✅ `members/mutations.ts` addMember/removeMember/updateMemberRole/transferOwnership DO write directly via adapter - - addMember: adapter.create (line 78) - - removeMember: adapter.deleteOne (line 189) - - updateMemberRole: adapter.updateMany (line 338, 425) - - transferOwnership: adapter.updateMany (line 415, 425) - -2. ✅ Org creation DOES insert owner member row - - Via Better Auth org plugin's implicit adapter.create in afterCreateOrganization hook (auth.ts:724) - - Member row created with role='owner' for the organization creator - -3. ✅ Invitation acceptance DOES insert member row - - Via Better Auth org plugin's implicit adapter.create in afterAcceptInvitation hook (auth.ts:767) - - Member row created with role specified in the invitation - -4. ✅ Org deletion DOES remove member rows - - Via Better Auth org plugin's implicit adapter.deleteMany when client calls authClient.organization.delete() - - Cascades handled by delete_cleanup.ts before deletion (line 79-80) - -SUMMARY OF ALL WRITERS: - -- 12 explicit code paths with adapter.create/deleteOne/updateMany for member -- 2 implicit Better Auth plugin hooks (afterCreateOrganization, afterAcceptInvitation) that create members -- 1 implicit Better Auth plugin operation (org deletion) that deletes members -- 1 migration that updates members -- Total: 16 distinct write paths to member table - WRITE PATHS: -- addMember: services/platform/convex/members/mutations.ts:78 - adapter.create member -- removeMember: services/platform/convex/members/mutations.ts:189 - adapter.deleteOne member -- updateMemberRole: services/platform/convex/members/mutations.ts:338 - adapter.updateMany member role -- updateMemberRole (owner demote): services/platform/convex/members/mutations.ts:425 - adapter.updateMany member role -- transferOwnership (promote): services/platform/convex/members/mutations.ts:415 - adapter.updateMany member role -- transferOwnership (demote): services/platform/convex/members/mutations.ts:425 - adapter.updateMany member role -- afterCreateOrganization: services/platform/convex/auth.ts:724 - Better Auth org plugin implicit adapter.create member -- afterAcceptInvitation: services/platform/convex/auth.ts:767 - Better Auth org plugin implicit adapter.create member -- findOrCreateSsoUser (existing user): services/platform/convex/sso_providers/find_or_create_sso_user.ts:118 - adapter.create member -- findOrCreateSsoUser (new user): services/platform/convex/sso_providers/find_or_create_sso_user.ts:175 - adapter.create member -- findOrCreateUserFromHeaders (existing org): services/platform/convex/betterAuth/trusted_headers/find_or_create_user_from_headers.ts:162 - adapter.create member -- findOrCreateUserFromHeaders (new org): services/platform/convex/betterAuth/trusted_headers/find_or_create_user_from_headers.ts:207 - adapter.create member -- addMemberInternal: services/platform/convex/users/add_member_internal.ts:30 - adapter.create member -- createMember (existing user): services/platform/convex/users/create_member.ts:126 - adapter.create member -- createMember (new user): services/platform/convex/users/create_member.ts:205 - adapter.create member -- createUserWithoutSession: services/platform/convex/users/create_user_without_session.ts:115 - adapter.create member -- organizationDelete: Better Auth client-side operation via authClient.organization.delete() - adapter.deleteMany member -- migrateOrgCreators: services/platform/convex/migrations/migrate_org_creators.ts:89 - adapter.updateMany member role - KEY FILES: services/platform/convex/members/mutations.ts, services/platform/convex/auth.ts, services/platform/convex/sso_providers/find_or_create_sso_user.ts, services/platform/convex/betterAuth/trusted_headers/find_or_create_user_from_headers.ts, services/platform/convex/users/add_member_internal.ts, services/platform/convex/users/create_member.ts, services/platform/convex/users/create_user_without_session.ts, services/platform/convex/organizations/delete_cleanup.ts, services/platform/convex/migrations/migrate_org_creators.ts - UNCOVERED/RISK PATHS: -- Better Auth organization plugin (afterCreateOrganization, afterAcceptInvitation hooks) - member creation happens inside Better Auth, not inline-instrumented -- Better Auth organization.delete() client-side call - member deletion happens inside Better Auth plugin, not inline-instrumented -- Any direct HTTP/REST endpoint mutations on Better Auth adapter not wrapped via internal.\* or components.betterAuth.adapter calls - ---- - -### AREA: Better Auth `teamMember` table write paths and team membership tracking - -Exhaustive inventory of all code paths that write to Better Auth's `teamMember` table and affect getUserTeamIds(): - -## Write Paths (ALL DIRECT MUTATIONS): - -1. **Team Member Addition** — `services/platform/convex/team_members/mutations.ts:73` - - Operation: `adapter.create` for `model: 'teamMember'` - - Function: `addMember()` mutation - - Requires: Admin or owner of org (enforced via getOrganizationMember RLS check at line 21) - - Creates: `{ teamId, userId, createdAt }` - -2. **Team Member Removal** — `services/platform/convex/team_members/mutations.ts:147` - - Operation: `adapter.deleteOne` for `model: 'teamMember'` - - Function: `removeMember()` mutation - - Requires: Admin of org OR self-removal (enforced at line 118) - - Prevents: Removing last team member (checked at line 141) - -3. **Entra ID SSO Team Sync — Team Member Add** — `services/platform/convex/sso_providers/entra_id/team_sync.ts:137` - - Operation: `adapter.create` for `model: 'teamMember'` - - Function: `addTeamMember()` (called by syncTeamsFromGroups at line 294) - - Triggered: During SSO Entra ID group synchronization flow - - Creates: `{ teamId, userId, createdAt }` - -4. **Entra ID SSO Team Sync — Stale Membership Removal** — `services/platform/convex/sso_providers/entra_id/team_sync.ts:213` - - Operation: `adapter.deleteOne` for `model: 'teamMember'` - - Function: `removeStaleTeamMemberships()` (called by syncTeamsFromGroups at line 304) - - Triggered: When user no longer in Entra ID group but still in local team - - Logic: Matches against `currentTeamNames` from SSO provider, deletes if not in list - -5. **Entra ID SSO Team Sync — Cascade Team Delete** — `services/platform/convex/sso_providers/entra_id/team_sync.ts:233` - - Operation: `adapter.deleteOne` for `model: 'team'` (cascades via Better Auth) - - Triggered: After last teamMember is removed (line 232 checks `remainingMembers.page.length === 0`) - - Side-effect: Better Auth's org plugin may auto-delete empty teams - -## How getUserTeamIds() Works: - -File: `services/platform/convex/lib/get_user_teams.ts` - -**JWT Short-circuit (Lines 61-83):** - -- Checks if `ctx.auth.getUserIdentity()` contains a `trustedTeams` claim (trusted headers mode) -- If present: Parses as JSON array of `{ id: string, name: string }` and returns `.map(t => t.id)` -- Returns early without querying the database — **DRIFT RISK**: JWT claim might diverge from actual teamMember rows - -**Fallback DB Query (Lines 86-104):** - -- If no trustedTeams JWT claim, queries Better Auth's `teamMember` adapter -- Paginates through all teamMember rows for the user (1000 items per page) -- Accumulates all `teamId` values via `m.teamId` mapping -- Returns complete list of team IDs user belongs to - -**Critical Code:** - -```typescript -// Trusted headers short-circuit: -const trustedTeamsRaw = getString(identity, 'trustedTeams'); -if (trustedTeamsRaw) { - const teams = parseJson>(trustedTeamsRaw); - return teams.map((t) => t.id); -} - -// DB query fallback: -const memberships = await ctx.runQuery(components.betterAuth.adapter.findMany, { - model: 'teamMember', - paginationOpts: { cursor, numItems: 1000 }, - where: [{ field: 'userId', operator: 'eq', value: userId }], -}); -allTeamIds.push(...memberships.page.map((m) => m.teamId)); -``` - -## Related Cascades (NOT writing teamMember, but related): - -1. **Org Deletion** — `services/platform/convex/organizations/delete_cleanup.ts:80` - - Does NOT explicitly delete teamMembers - - Better Auth org plugin likely cascades this (not visible in custom code) - - Personalization cascade (`cascadeOnOrgDeleted`) deletes userMemories + userPreferences - -2. **Org Member Removal** — `services/platform/convex/members/mutations.ts:189-197` - - Deletes organization `member` (NOT `teamMember`) - - Calls `cascadeOnMemberRemoved()` which deletes userMemories + userPreferences + TTS chunks - - Does NOT explicitly clean up user's teamMembers in teams under that org - -## Mutation Authorization: - -- `addMember()`: Requires org admin/owner role (line 27 check) -- `removeMember()`: Requires org admin/owner OR self-removal (line 118 check) -- Entra ID sync: Automatic via SSO flow (no explicit permission checks in sync code, trusts provider) - -## Schema Indexes: - -File: `services/platform/convex/betterAuth/schema.ts:45-47` - -- Custom composite index on `teamMember.teamId_userId` for efficient lookups - -## No Explicit Organization-to-Team Cascades Found: - -Better Auth's `organization.delete()` endpoint (called from `/deleteOrganization` mutation) is NOT overridden with custom cascading logic in the codebase. It likely: - -- Uses Better Auth's built-in organization plugin cascade behavior -- May automatically delete `team` rows (not confirmed in custom code) -- May cascade `teamMember` deletion (NOT explicitly verified in codebase) - -WRITE PATHS: - -- team_members/mutations.ts:73 / adapter.create(teamMember) / addMember() mutation -- team_members/mutations.ts:147 / adapter.deleteOne(teamMember) / removeMember() mutation -- sso_providers/entra_id/team_sync.ts:137 / adapter.create(teamMember) / addTeamMember() called by syncTeamsFromGroups() -- sso_providers/entra_id/team_sync.ts:213 / adapter.deleteOne(teamMember) / removeStaleTeamMemberships() via syncTeamsFromGroups() -- sso_providers/entra_id/team_sync.ts:233 / adapter.deleteOne(team) / cascade delete when last member removed - KEY FILES: services/platform/convex/team_members/mutations.ts, services/platform/convex/lib/get_user_teams.ts, services/platform/convex/sso_providers/entra_id/team_sync.ts, services/platform/convex/members/mutations.ts, services/platform/convex/organizations/delete_cleanup.ts, services/platform/convex/auth.ts, services/platform/convex/betterAuth/schema.ts - UNCOVERED/RISK PATHS: -- Better Auth organization.delete() built-in cascades NOT reviewed in custom code — unclear if teamMember rows are auto-deleted when org deletes (potential orphan rows or silent cascades) -- No explicit Better Auth hook override for afterRemoveOrganizationMember — org member removal does NOT explicitly delete user's teamMembers (user's team memberships survive org exit) -- JWT trustedTeams claim lifecycle NOT covered — no code showing how team IDs enter or update trustedTeams JWT field; mismatch between JWT claim and DB state possible -- SSO group-to-team sync is ONLY entrypoint for team creation via Entra ID — teams created via the web UI (if any) NOT instrumented (unknown if those exist) -- No visible script/migration handling teamMember cascades during schema evolution — unclear how past team/member bulk operations affected membership state -- invitation table has optional teamId field (per schema) — acceptInvitation hook invites into team but NOT explicitly shown creating teamMember rows (invitation→team flow unclear) - ---- - -### AREA: Better Auth Built-in Organization/Team Endpoints & Client Accessibility - -## Complete Endpoint Audit - -### Part 1: Better Auth Built-in Member/Team Mutation Endpoints - -Better Auth exposes the following member and teamMember-mutating endpoints (all in `/organization/*` path): - -**Organization-level member mutations:** - -1. `/organization/invite-member` (POST) - inviteMember via authClient.organization.inviteMember() -2. `/organization/accept-invitation` (POST) - acceptInvitation via authClient.organization.acceptInvitation() -3. `/organization/remove-member` (POST) - removeMember via authClient.organization.removeMember() -4. `/organization/update-member-role` (POST) - updateMemberRole via authClient.organization.updateMemberRole() -5. `/organization/leave` (POST) - leaveOrganization via authClient.organization.leaveOrganization() -6. `/organization/add-member` (POST) - NOT directly exposed in authClient (internal endpoint only) - -**Team-level member mutations:** - -1. `/organization/add-team-member` (POST) - addTeamMember via authClient.organization.addTeamMember() -2. `/organization/remove-team-member` (POST) - removeTeamMember via authClient.organization.removeTeamMember() - -**Team mutations affecting organization context:** - -1. `/organization/create-team` (POST) - createTeam via authClient.organization.createTeam() -2. `/organization/remove-team` (POST) - removeTeam via authClient.organization.removeTeam() -3. `/organization/update-team` (POST) - updateTeam via authClient.organization.updateTeam() - -**Organization mutations:** - -1. `/organization/create` (POST) - createOrganization via authClient.organization.create() -2. `/organization/update` (POST) - updateOrganization via authClient.organization.update() -3. `/organization/delete` (POST) - deleteOrganization via authClient.organization.delete() -4. `/organization/set-active` (POST) - setActiveOrganization via authClient.organization.setActive() - -**Client-side usage in Tale codebase** (from grep results in /app and /lib): - -- authClient.organization.create() - used in organization-form.tsx, dashboard/index.tsx -- authClient.organization.delete() - used in organization-list-panel.tsx -- authClient.organization.update() - used in organization-settings.tsx -- authClient.organization.setActive() - used in organization-form.tsx, dashboard routes, switching.tsx -- authClient.organization.createTeam() - used in team-create-dialog.tsx -- authClient.organization.removeTeam() - used in team-delete-dialog.tsx -- authClient.organization.updateTeam() - used in team-edit-dialog.tsx - -Tale's custom Convex mutations intercept some operations (removeMember, updateMemberRole, addMember, removeMember for teams) but Better Auth's built-in endpoints are ALSO callable directly by the client. - -### Part 2: Better Auth Hooks & What Data is Available - -The organization plugin defines these **organizationHooks** that fire (from auth.ts lines 584-791): - -**Member-related hooks (called AFTER database mutation):** - -- afterCreateOrganization: data = { organization, user, member } -- afterAcceptInvitation: data = { organizationId, userId, userEmail, userRole } (via logJoinedOrganization audit call) -- afterRemoveMember: data = { member, user, organization } -- afterUpdateMemberRole: data = { member, previousRole, user, organization } -- afterAddMember: data = { member, user, organization } -- afterAddTeamMember: data = { teamMember, team, user, organization } -- afterRemoveTeamMember: data = { teamMember, team, user, organization } - -**Middleware Context (mw object in `hooks.after` createAuthMiddleware):** - -The `after: createAuthMiddleware(async (mw) => {...})` middleware on lines 463-553 has access to: - -- mw.path: The request path string (e.g., '/organization/remove-member', '/organization/accept-invitation') -- mw.body: The request body as an object (contains memberId, email, organizationId, userId, teamId, etc.) -- mw.context.returned: The endpoint's returned value (the created/updated/deleted record, or an APIError on failure) -- mw.context.newSession: The newly created session (if sign-in/up endpoint) -- mw.context.session: The current session context -- mw.request: The raw Request object (for headers, method, etc.) -- mw.method: The HTTP method - -**CRITICAL FINDING: The after-middleware can detect org/team membership mutations by path:** - -For member writes, the exact path strings are: - -- `/organization/invite-member` - mw.body has { email, role, organizationId, teamId }. Returns { id, email, role, organizationId, inviterId, status, expiresAt } -- `/organization/accept-invitation` - mw.body has { invitationId }. Returns { invitation, member }. mw.context.returned.member has { organizationId, userId, role } -- `/organization/remove-member` - mw.body has { memberIdOrEmail, organizationId }. Returns { member: { id, userId, organizationId, role } } -- `/organization/update-member-role` - mw.body has { role, memberId, organizationId }. Returns { member: { id, userId, organizationId, role } } -- `/organization/leave` - mw.body has { organizationId }. Returns { member: { userId, organizationId, role } } - -For team member writes: - -- `/organization/add-team-member` - mw.body has { teamId, userId, organizationId }. Returns { id, userId, teamId, createdAt } -- `/organization/remove-team-member` - mw.body has { teamId, userId, organizationId }. Returns { message: "Team member removed successfully." } - -For team mutations: - -- `/organization/create-team` - mw.body has { name, organizationId }. Returns { id, name, organizationId, createdAt, updatedAt } -- `/organization/remove-team` - mw.body has { teamId, organizationId }. Returns { message: "Team removed successfully." } -- `/organization/update-team` - mw.body has { teamId, data: { name, ... } }. Returns { id, name, organizationId, createdAt, updatedAt } - -**Data recovery for sync:** From mw.body and mw.context.returned, an after-middleware can reliably extract: - -- organizationId (present in body for most endpoints) -- userId (present in body for team operations or in returned.member.userId for member operations) -- teamId (present in body for team operations, in returned.teamId for team member ops) -- member.role (for role changes, in returned.member.role or returned.role) - -### Part 3: Can Tale Disable Built-in Endpoints? - -**The answer is NO — Better Auth's organization plugin does NOT provide explicit endpoint disabling via plugin config.** - -Evidence: - -1. The organization plugin config (types.d.mts, lines 8-144) has no `disabledEndpoints`, `endpointFilter`, or access control mechanism to turn off specific endpoints. -2. The only access control (ac: AccessControl) gates role-based permissions WITHIN endpoints (e.g., hasPermission check at crud-members.mjs:170-175), but does NOT prevent endpoint registration. -3. There is NO config option like `disableBuiltInEndpoints`, `customMutationsOnly`, or `requireCustomMutations`. -4. Attempting to intercept at the plugin level or middleware level would require custom patching of the plugin code. - -**Workaround status:** Tale would need to: - -- Either use a custom reverse-proxy/middleware at the auth API boundary to block specific paths -- Or override the endpoints map in the plugin initialization (not officially supported) -- Or use the after-middleware to detect unauthorized calls and reject them (defensive) - -The only pragmatic catch-all is to hook the after-middleware to detect org/team membership mutations by path and re-sync Convex mirrors (which mitigates the drift risk). - -### Part 4: After-Middleware Reliability for Catching All Mutations - -**YES, the after-middleware CAN reliably detect and recover context for ANY member/teamMember-mutating endpoint:** - -The after-hook fires for all endpoints regardless of success/failure (before the response is sent), and the `mw.path` string is deterministic. The exact paths to monitor for drift-sync are: - -**Member sync paths:** - -- `/organization/invite-member` → track invitations -- `/organization/accept-invitation` → create member record -- `/organization/remove-member` → delete member record -- `/organization/update-member-role` → update member.role -- `/organization/leave` → delete member (self-removal) - -**TeamMember sync paths:** - -- `/organization/add-team-member` → create teamMember record -- `/organization/remove-team-member` → delete teamMember record - -**Team sync paths** (affects org structure): - -- `/organization/create-team` → create team record -- `/organization/remove-team` → delete team (cascade to teamMembers) -- `/organization/update-team` → update team record - -The extracted context (organizationId, userId, teamId) is sufficient to re-sync the Convex mirror via a background action triggered by the after-hook. -WRITE PATHS: - -- '/organization/create' - POST - authClient.organization.create() -- '/organization/update' - POST - authClient.organization.update() -- '/organization/delete' - POST - authClient.organization.delete() -- '/organization/set-active' - POST - authClient.organization.setActive() -- '/organization/invite-member' - POST - authClient.organization.inviteMember() -- '/organization/accept-invitation' - POST - authClient.organization.acceptInvitation() -- '/organization/remove-member' - POST - authClient.organization.removeMember() -- '/organization/update-member-role' - POST - authClient.organization.updateMemberRole() -- '/organization/leave' - POST - authClient.organization.leaveOrganization() -- '/organization/create-team' - POST - authClient.organization.createTeam() -- '/organization/remove-team' - POST - authClient.organization.removeTeam() -- '/organization/update-team' - POST - authClient.organization.updateTeam() -- '/organization/add-team-member' - POST - authClient.organization.addTeamMember() -- '/organization/remove-team-member' - POST - authClient.organization.removeTeamMember() - KEY FILES: services/platform/convex/auth.ts, services/platform/convex/members/mutations.ts, services/platform/convex/team_members/mutations.ts, node_modules/better-auth/dist/plugins/organization/routes/crud-members.mjs, node_modules/better-auth/dist/plugins/organization/routes/crud-invites.mjs, node_modules/better-auth/dist/plugins/organization/routes/crud-org.mjs, node_modules/better-auth/dist/plugins/organization/routes/crud-team.mjs - UNCOVERED/RISK PATHS: -- Better Auth organization plugin has NO endpoint disabling/access-control config — all member/team mutation endpoints are always registered and reachable by any authenticated user with org membership -- Tale's after-middleware (auth.ts:463-553) currently does NOT sync member/teamMember mutations back to Convex — only signs-in, 2FA, and API key suffix updates -- Client-side calls to authClient.organization.\* bypass Tale's custom Convex mutations entirely (e.g., removeMember, updateMemberRole) — no unified audit trail -- No existing catch-all path for /organization/\* changes — drift can occur undetected when client calls Better Auth endpoints directly instead of Convex mutations -- The Better Auth adapter persists to Convex tables directly (member, teamMember, organization), but no Convex RLS/cascade hooks fire on those writes — inconsistency risk -- Team-level membership (teamMember) mutations are NOT gated by Tale's audit/governance system — no legal hold checks on team member removal - ---- - -### AREA: Org/Team Membership Read Contract for Mirror Implementation - -Mapped complete caller inventory: 8 direct callers of getUserOrganizations (users/queries, members/queries×2, prompts/queries×5, lib/rls/helpers×3, update_user_password), 13 callers of getOrganizationMember (organizations/record_org_switch, organizations/delete_cleanup, openai_compat, tasks/queries, tasks/mutations, video_links/queries, video_links/mutations, projects/queries, projects/mutations, projects/internal_queries, projects/secrets/internal, team_members/mutations, members/queries), 8 callers of getUserTeamIds (tasks/queries, tasks/mutations, projects/queries, projects/mutations, projects/internal_queries, projects/secrets/internal, lib/rls/helpers/rls_rules, lib/rls/helpers/z_query_with_rls). Return schema: getUserOrganizations returns Array<{organizationId, role: MemberRole, member: OrganizationMember}>; getOrganizationMember returns single OrganizationMember; getUserTeamIds returns Array. OrganizationMember fields: \_id (member ID), createdAt (timestamp), organizationId (org ID), userId (user ID), role (role string). Field consumption in rls_rules.ts (line 65-68, repeated 50+ times): member.role accessed via membership?.role for authorizeRls permission matrix across 40+ table types. Field consumption in members/queries.ts: member.\_id (returned in getCurrentMemberContext line 82), member.organizationId (line 83), member.userId (line 84), member.role (line 78 validated), member.createdAt (line 86 returned). Field consumption in tasks/queries.ts: member.userId passed to getUserTeamIds (line 10), member.role returned in context (line 11). Field consumption in projects/queries.ts: member.userId passed to getUserTeamIds, member.role returned in context. Field consumption in record_org_switch.ts: member.role captured in audit log (line 87). Field consumption in delete_cleanup.ts: member.role checked for owner-only (line 143). All 5 fields accessed downstream — no optional fields. Role normalization: VALID_ROLES = {owner, disabled, member, editor, developer, admin} (get_user_organizations.ts line 12-19, members/queries.ts line 35-42, access_control.ts line 61-228). Normalization pipeline: raw member.role → trustedData?.trustedRole override (line 85 get_user_organizations.ts) → toLowerCase() (line 86) → isValidRole check (line 87) → fallback to 'member' (line 88-89) → filter disabled in return (line 99). Disabled role handling: rows with role=disabled filtered from getUserOrganizations output (line 99), checked with throw in getOrganizationMember (line 85-88). Trusted headers interaction (getTrustedAuthData in auth/get_trusted_auth_data.ts): JWT contains trustedRole claim; this OVERRIDES member.role at consumption time in getUserOrganizations line 85 (raw role = trustedData?.trustedRole || member.role). Mirror does NOT store trustedRole — trust layer provides override. Active org derivation (record_org_switch.ts): member.role captured in audit log when org switched (line 87), lastActiveOrganizationId persisted on user record (line 103), but role comes from member row, not stored on user. Owner→Admin mapping (access_control.ts line 247-251): 'owner' normalized to 'admin' permission level in matrix. Email fallback in getOrganizationMember (line 44-77): triggered on userId mismatch (account migrations, social linking). Performs: (1) query member by (organizationId, userId) → fails, (2) if authUser.email exists, fallback query user by email, (3) query member by (organizationId, resolved userId). Throws UnauthorizedError if still no match. STAYS ON DB PATH: email fallback requires user table lookups (not mirrored), two sequential queries (rare error path), cross-table resolution complexity not worth replicating in mirror. -WRITE PATHS: - -- KEY FILES: services/platform/convex/lib/rls/organization/get_user_organizations.ts, services/platform/convex/lib/rls/organization/get_organization_member.ts, services/platform/convex/lib/get_user_teams.ts, services/platform/convex/lib/rls/helpers/rls_rules.ts, services/platform/convex/lib/rls/helpers/access_control.ts, services/platform/convex/lib/rls/auth/get_trusted_auth_data.ts, services/platform/convex/members/queries.ts, services/platform/convex/organizations/record_org_switch.ts, services/platform/convex/lib/rls/types.ts, services/platform/convex/members/validators.ts - UNCOVERED/RISK PATHS: -- Email fallback path in getOrganizationMember (line 50-77) requires user table email lookups which are not mirrored; mirror cannot serve this path — fallback must remain on DB path during account migrations/linking -- Trusted role override (getTrustedAuthData in JWT) is not stored in mirror; JWT claims layer must override role at consumption time in getUserOrganizations — mirror provides DB source only -- Cascading team membership changes (teamMember table) require separate getUserTeamIds queries with 1000-item pagination; not denormalized into member table, requires separate mirror or DB path for team isolation -- RLS rules engine (rls_rules.ts lines 65-68, 74-76, etc.) performs 50+ membership lookups per request; current prefetch in z_query_with_rls.ts + z_mutation_with_rls.ts + request_auth_cache.ts parallelizes batch queries, reducing latency incrementally if mirror co-located, but doesn't eliminate round-trip to adapter -- Disabled role filtering happens after normalization (line 99); mirror could pre-filter, but RLS rules intentionally preserve disabled rows and gate via permission matrix (disabled role → NONE for all 40+ tables), so mirror must store disabled members unchanged to preserve filtering semantics - ---- - -### AREA: Convex Mirror Table Infrastructure Patterns (Platform services/platform/convex) - -## 1. Schema Composition Pattern (Root + Feature Modules) - -**Location:** services/platform/convex/schema.ts (lines 1-217) - -The root schema uses a modular import pattern where each feature module (agents/, tasks/, governance/, etc.) exports its table(s) from a feature-specific schema.ts file, then the root schema.ts imports and composes them into a single defineSchema object. - -**Pattern:** - -```typescript -// Feature module: convex/feature_name/schema.ts -import { defineTable } from 'convex/server'; -import { v } from 'convex/values'; - -export const featureTable = defineTable({ - organizationId: v.string(), - // ... fields -}) - .index('by_organization', ['organizationId']) - .index('by_feature_key', ['organizationId', 'someField']); - -// Root schema.ts -import { featureTable } from './feature_name/schema'; -export default defineSchema({ - featureTable: featureTable, - // ... other tables -}); -``` - -**For a mirror table**, the pattern is identical: - -- Define in /convex/feature_name/schema.ts (or new feature if creating from scratch) -- Import into root schema.ts at line ~50-115 -- Add to the defineSchema object at line ~118-217 -- Use feature-consistent naming: e.g., `memberMirrorTable` if mirroring betterAuth members - -## 2. Migration Patterns (One-Shot Backfill) - -**Locations:** - -- services/platform/convex/migrations.ts (entry point) -- services/platform/convex/migrations/\*.ts (individual migrations) - -The migration framework uses @convex-dev/migrations with a two-phase pattern: - -1. Individual `internalMutation` or `internalAction` files in migrations/ directory -2. Orchestration via the `runAll` internalAction in migrations.ts, which calls each migration sequentially - -**Cursor-based pagination pattern (used by backfills reading large tables):** - -From backfill_thread_metadata.ts (lines 19-111): - -```typescript -const USERS_PAGE_SIZE = 100; -const THREADS_PAGE_SIZE = 200; - -export const backfillThreadMetadata = internalMutation({ - args: {}, - handler: async (ctx) => { - let created = 0; - let skipped = 0; - let userCursor: string | null = null; - let usersDone = false; - - while (!usersDone) { - const usersResult = await ctx.runQuery(components.agent.users.listUsersWithThreads, { - paginationOpts: { cursor: userCursor, numItems: USERS_PAGE_SIZE }, - }); - - for (const userId of usersResult.page) { - let threadCursor: string | null = null; - let threadsDone = false; - - while (!threadsDone) { - const threadsResult = await ctx.runQuery(components.agent.threads.listThreadsByUserId, { - userId, - order: 'desc', - paginationOpts: { cursor: threadCursor, numItems: THREADS_PAGE_SIZE }, - }); - - for (const thread of threadsResult.page) { - const existing = await ctx.db - .query('threadMetadata') - .withIndex('by_threadId', (q) => q.eq('threadId', thread._id)) - .first(); - if (existing) { - skipped++; - continue; - } - await ctx.db.insert('threadMetadata', {...}); - created++; - } - - threadCursor = threadsResult.continueCursor; - threadsDone = threadsResult.isDone; - } - } - - userCursor = usersResult.continueCursor; - usersDone = usersResult.isDone; - } - - return { created, skipped }; - }, -}); -``` - -**Simpler local-table pagination pattern** (backfill_folders.ts, lines 21-108): - -```typescript -const BATCH_SIZE = 200; - -export const backfillFolders = internalMutation({ - args: {}, - handler: async (ctx) => { - let totalUpdated = 0; - let totalSkipped = 0; - let cursor: string | null = null; - let isDone = false; - - while (!isDone) { - const result = await ctx.db - .query('documents') - .paginate({ cursor, numItems: BATCH_SIZE }); - - for (const doc of result.page) { - if (doc.folderId) { - skipped++; - continue; - } - // process doc... - await ctx.db.patch(doc._id, { folderId }); - updated++; - } - - cursor = result.continueCursor; - isDone = result.isDone; - } - return { updated: totalUpdated, skipped: totalSkipped }; - }, -}); -``` - -**For betterAuth component reads** (migrate_org_creators.ts, lines 26-131): - -```typescript -const orgsResult = await ctx.runQuery(components.betterAuth.adapter.findMany, { - model: 'organization', // or 'member', 'user', 'session', 'teamMember' - paginationOpts: { cursor: null, numItems: 500 }, - where: [], // optional filtering -}); - -for (const orgRaw of orgsResult.page) { - const membersResult = await ctx.runQuery( - components.betterAuth.adapter.findMany, - { - model: 'member', - paginationOpts: { cursor: null, numItems: 100 }, - where: [{ field: 'organizationId', value: orgId, operator: 'eq' }], - }, - ); - // Process membersResult.page... -} -``` - -**Template for backfill that reads betterAuth member/teamMember and upserts mirror:** - -File: /convex/migrations/backfill_member_mirror.ts - -```typescript -/** - * Migration: Backfill memberMirror table from betterAuth members and teamMembers. - * - * Reads all organizations, then for each org: - * 1. Fetches all members with findMany(model: 'member') - * 2. Fetches all teamMembers with findMany(model: 'teamMember') - * 3. Upserts rows into memberMirror table - * - * Idempotent: skips records that already exist in memberMirror. - * - * Usage: - * bunx convex run migrations/backfill_member_mirror:apply - */ - -import { isRecord, getString } from '../../lib/utils/type-guards'; -import { components, internal } from '../_generated/api'; -import { internalMutation, internalAction } from '../_generated/server'; -import { v } from 'convex/values'; - -const BATCH_SIZE = 100; -const ORGS_BATCH = 50; - -export const backfillMembers = internalMutation({ - args: { organizationId: v.string() }, - returns: v.object({ created: v.number(), skipped: v.number() }), - handler: async (ctx, args) => { - let created = 0; - let skipped = 0; - - let memberCursor: string | null = null; - let membersDone = false; - - while (!membersDone) { - const membersResult: { - page: Record[]; - isDone: boolean; - continueCursor: string; - } = await ctx.runQuery(components.betterAuth.adapter.findMany, { - model: 'member', - paginationOpts: { cursor: memberCursor, numItems: BATCH_SIZE }, - where: [ - { - field: 'organizationId', - value: args.organizationId, - operator: 'eq', - }, - ], - }); - - for (const memberRaw of membersResult.page) { - const member = isRecord(memberRaw) ? memberRaw : undefined; - if (!member) { - skipped++; - continue; - } - - const memberId = getString(member, '_id'); - const userId = getString(member, 'userId'); - const organizationId = getString(member, 'organizationId'); - const role = getString(member, 'role'); - - if (!memberId || !userId || !organizationId) { - skipped++; - continue; - } - - const existing = await ctx.db - .query('memberMirror') - .withIndex('by_memberid', (q) => q.eq('memberId', memberId)) - .first(); - - if (existing) { - skipped++; - continue; - } - - await ctx.db.insert('memberMirror', { - organizationId, - memberId, - userId, - role: role ?? 'member', - createdAt: Date.now(), - }); - created++; - } - - memberCursor = membersResult.continueCursor; - membersDone = membersResult.isDone; - } - - return { created, skipped }; - }, -}); - -export const apply = internalAction({ - args: {}, - returns: v.object({ totalCreated: v.number(), totalSkipped: v.number() }), - handler: async (ctx) => { - let totalCreated = 0; - let totalSkipped = 0; - - let orgCursor: string | null = null; - let orgsDone = false; - - while (!orgsDone) { - const orgsResult: { - page: Record[]; - isDone: boolean; - continueCursor: string; - } = await ctx.runQuery(components.betterAuth.adapter.findMany, { - model: 'organization', - paginationOpts: { cursor: orgCursor, numItems: ORGS_BATCH }, - where: [], - }); - - for (const orgRaw of orgsResult.page) { - const org = isRecord(orgRaw) ? orgRaw : undefined; - const orgId = org ? getString(org, '_id') : undefined; - if (!orgId) continue; - - const result: { created: number; skipped: number } = - await ctx.runMutation( - internal.migrations.backfill_member_mirror.backfillMembers, - { organizationId: orgId }, - ); - totalCreated += result.created; - totalSkipped += result.skipped; - } - - orgCursor = orgsResult.continueCursor; - orgsDone = orgsResult.isDone; - } - - console.log('[backfill_member_mirror] done', { - totalCreated, - totalSkipped, - }); - return { totalCreated, totalSkipped }; - }, -}); -``` - -Then register in migrations.ts: - -```typescript -export const runAll = internalAction({ - args: {}, - handler: async (ctx) => { - // ... existing migrations ... - await ctx.runMutation(internal.migrations.backfill_member_mirror.apply, {}); - }, -}); -``` - -## 3. Cron Patterns (Periodic Reconciliation) - -**Location:** services/platform/convex/crons.ts (lines 1-141) - -Convex cron syntax using cronJobs() from convex/server. Each cron maps a cron expression to an internal mutation or action. - -**Bounded/paginated cron example** (tts/cascade_helpers.ts, lines 159-248): - -The TTS GC cron demonstrates the pattern for reconciliation at scale: - -1. Maintain a cursor in a singleton table (ttsGcCursor) between runs -2. Probe with first() to find the next distinct org -3. Apply bounded work (ROWS_PER_ORG_PER_RUN) per org -4. Budget-aware: only count orgs that actually had work (skip-empty optimization) -5. Wrap around when reaching the end - -**Template for member mirror reconciliation cron:** - -File: /convex/members/mirror_reconciliation.ts - -```typescript -/** - * Hourly reconciliation cron: compare memberMirror table against betterAuth - * members and teamMembers, repair drift (deleted users, role changes). - * - * Bounded to MAX_ORGS_PER_RUN × MEMBERS_PER_ORG so one tenant doesn't starve - * others. Cursor persists in memberMirrorGcCursor singleton. - */ - -import { v } from 'convex/values'; -import { internalMutation } from '../_generated/server'; -import { components } from '../_generated/api'; - -const MEMBERS_PER_ORG = 200; -const MAX_ORGS_PER_RUN = 20; -const GC_CURSOR_JOB = 'memberMirrorReconcile'; - -export const reconcileMemberMirror = internalMutation({ - args: {}, - returns: v.object({ - orgsScanned: v.number(), - rowsDeleted: v.number(), - rowsUpdated: v.number(), - wrappedAround: v.boolean(), - }), - handler: async (ctx) => { - let orgsScanned = 0; - let rowsDeleted = 0; - let rowsUpdated = 0; - let wrappedAround = false; - - const cursorRow = await ctx.db - .query('memberMirrorGcCursor') - .withIndex('by_job', (q) => q.eq('job', GC_CURSOR_JOB)) - .first(); - let cursor: string | null = cursorRow?.lastOrgId ?? null; - - while (orgsScanned < MAX_ORGS_PER_RUN) { - // Phase 1: Probe to find the next org with mirror rows. - const probe = await ctx.db - .query('memberMirror') - .withIndex('by_organizationId', (q) => - cursor === null ? q : q.gt('organizationId', cursor), - ) - .first(); - - if (!probe) { - // No more orgs. Wrap to start. - if (cursor !== null) { - cursor = null; - wrappedAround = true; - } - break; - } - - const orgId = probe.organizationId; - cursor = orgId; - - // Phase 2: Fetch current members from betterAuth for this org. - const betterAuthMembers = new Map(); - let authCursor: string | null = null; - let authDone = false; - - while (!authDone) { - const result = await ctx.runQuery( - components.betterAuth.adapter.findMany, - { - model: 'member', - paginationOpts: { cursor: authCursor, numItems: MEMBERS_PER_ORG }, - where: [{ field: 'organizationId', value: orgId, operator: 'eq' }], - }, - ); - - for (const memberRaw of result.page ?? []) { - const memberId = memberRaw?._id; - if (memberId) { - betterAuthMembers.set(memberId, memberRaw); - } - } - - authCursor = result.continueCursor; - authDone = result.isDone; - } - - // Phase 3: Scan mirror rows and repair drift. - const mirrorMembers = await ctx.db - .query('memberMirror') - .withIndex('by_organizationId', (q) => q.eq('organizationId', orgId)) - .take(MEMBERS_PER_ORG); - - for (const mirrorRow of mirrorMembers) { - const betterAuthRow = betterAuthMembers.get(mirrorRow.memberId); - - if (!betterAuthRow) { - // Member deleted in betterAuth — delete mirror. - await ctx.db.delete(mirrorRow._id); - rowsDeleted++; - continue; - } - - // Check for drift (role change, userId change). - const newRole = betterAuthRow.role ?? 'member'; - const newUserId = betterAuthRow.userId; - - if (mirrorRow.role !== newRole || mirrorRow.userId !== newUserId) { - await ctx.db.patch(mirrorRow._id, { - role: newRole, - userId: newUserId, - updatedAt: Date.now(), - }); - rowsUpdated++; - } - } - - // Skip-empty optimization: only count orgs that had any drift. - if (mirrorMembers.length > 0) { - orgsScanned += 1; - } - } - - // Persist cursor for next run. - const updatedAt = Date.now(); - if (cursorRow) { - await ctx.db.patch(cursorRow._id, { lastOrgId: cursor, updatedAt }); - } else { - await ctx.db.insert('memberMirrorGcCursor', { - job: GC_CURSOR_JOB, - lastOrgId: cursor, - updatedAt, - }); - } - - console.info('[memberMirror.reconcile] done', { - orgsScanned, - rowsDeleted, - rowsUpdated, - wrappedAround, - }); - - return { orgsScanned, rowsDeleted, rowsUpdated, wrappedAround }; - }, -}); -``` - -Then register in crons.ts (around line 140): - -```typescript -crons.cron( - 'reconcile member mirror (hourly)', - '0 * * * *', - internal.members.mirror_reconciliation.reconcileMemberMirror, - {}, -); -``` - -## 4. Existing Denormalization / Mirror Patterns in Schema - -**Location:** services/platform/convex/tasks/schema.ts (lines 54-115) - -The tasks table denormalizes `commentCount` (line 79-83): - -```typescript -// Denormalized count of non-deleted comments, maintained by the comment -// add/delete mutations so the board/table can render a comment indicator -// without an N+1 fetch. Optional for back-compat with tasks created before -// counting (treat undefined as 0). -commentCount: v.optional(v.number()), -``` - -**Write-path maintenance** (/convex/tasks/mutations.ts): - -```typescript -// Keep the denormalized comment count in step with the live comment set. -await ctx.db.patch(args.taskId, { - commentCount: (task.commentCount ?? 0) + 1, -}); - -// On soft-delete: -await ctx.db.patch(comment.taskId, { - commentCount: Math.max(0, (task.commentCount ?? 0) - toDelete.length), -}); -``` - -**Backfill pattern for denormalization** (/convex/tasks/internal_mutations.ts): - -```typescript -export const backfillTaskCommentCounts = internalMutation({ - args: { organizationId: v.string() }, - returns: v.object({ scanned: v.number(), updated: v.number() }), - handler: async (ctx, args) => { - let scanned = 0; - let updated = 0; - - for await (const task of ctx.db - .query('tasks') - .withIndex('by_organization', (q) => - q.eq('organizationId', args.organizationId), - )) { - scanned++; - let count = 0; - for await (const comment of ctx.db - .query('taskComments') - .withIndex('by_task_createdAt', (q) => q.eq('taskId', task._id))) { - if (!comment.deletedAt) count += 1; - } - if ((task.commentCount ?? 0) !== count) { - await ctx.db.patch(task._id, { commentCount: count }); - updated += 1; - } - } - return { scanned, updated }; - }, -}); -``` - -**Another denormalization example**: threadMetadata (lines 14-157 in /convex/threads/schema.ts) is itself a mirror/shadow of agent component threads, with fields like threadId, userId, chatType denormalized from the source. - -The pattern for maintaining a mirror in write paths: - -1. When source (betterAuth member) changes → patch mirror row -2. Batch updates via `updateMany` if touching multiple rows -3. Use `withIndex()` to find mirror rows by source ID -4. Clamp numeric denormalizations at 0 to prevent negative underflow - -## 5. Key Files and Reusable Snippets - -**Table definition with indexes:** - -```typescript -// /convex/members/schema.ts -import { defineTable } from 'convex/server'; -import { v } from 'convex/values'; - -export const memberMirrorTable = defineTable({ - organizationId: v.string(), - memberId: v.string(), // betterAuth member._id - userId: v.string(), // betterAuth user._id - role: v.string(), // 'owner', 'admin', 'member' - createdAt: v.number(), - updatedAt: v.optional(v.number()), -}) - .index('by_organizationId', ['organizationId']) - .index('by_memberId', ['memberId']) - .index('by_org_user', ['organizationId', 'userId']); - -export const memberMirrorGcCursorTable = defineTable({ - job: v.string(), // singleton key: 'memberMirrorReconcile' - lastOrgId: v.optional(v.string()), - updatedAt: v.number(), -}).index('by_job', ['job']); -``` - -**Querying denormalized field for N+1 prevention:** - -```typescript -// Instead of: -// for (const task of tasks) { -// const comments = await ctx.db.query('taskComments').withIndex('by_task_createdAt', q => q.eq('taskId', task._id)).take(10); -// task.commentCount = comments.length; // N+1 query -// } - -// Use denormalized field: -for (const task of tasks) { - const commentCount = task.commentCount ?? 0; // Already on the row -} -``` - -**Pagination cursor reset pattern (for actions that orchestrate multiple mutations):** - -```typescript -// From split_personalization_toggle.ts: each mutation handles one paginated -// table, but the enclosing action can call multiple mutations sequentially -// because Convex caps each function at ONE paginated query. - -export const apply = internalAction({ - handler: async (ctx) => { - const prefsResult = await ctx.runMutation( - internal.migrations.split_personalization_toggle.applyUserPrefs, - {}, // applyUserPrefs has its own pagination loop - ); - const policyResult = await ctx.runMutation( - internal.migrations.split_personalization_toggle.applyOrgPolicies, - {}, // applyOrgPolicies has its own pagination loop - ); - return { ...prefsResult, ...policyResult }; - }, -}); -``` - -WRITE PATHS: - -- KEY FILES: services/platform/convex/schema.ts, services/platform/convex/migrations.ts, services/platform/convex/migrations/backfill_thread_metadata.ts, services/platform/convex/migrations/backfill_folders.ts, services/platform/convex/migrations/split_personalization_toggle.ts, services/platform/convex/migrations/migrate_org_creators.ts, services/platform/convex/crons.ts, services/platform/convex/tts/cascade_helpers.ts, services/platform/convex/tasks/schema.ts, services/platform/convex/tasks/mutations.ts, services/platform/convex/tasks/internal_mutations.ts, services/platform/convex/threads/schema.ts, services/platform/convex/betterAuth/adapter.ts - UNCOVERED/RISK PATHS: -- Write-path hooks that automatically maintain mirror tables (e.g., when betterAuth members are mutated via betterAuth.adapter.updateMany, the mirror row updates are NOT automatically cascaded — they must be manually triggered via a separate Convex mutation or a reconciliation cron) -- Hook-based synchronization between betterAuth and mirror tables — currently not available as a built-in; drift reconciliation requires periodic cron sweeps -- Webhook endpoints from betterAuth for real-time member/teamMember mutations — the adapter findMany/findOne is query-based only - -## Architect spec - -Perfect. Now let me produce the comprehensive implementation spec based on all the information I've gathered: - -## COMPLETE IMPLEMENTATION SPEC: Convex-Native Membership Mirror - -Based on exhaustive enumeration of all member/teamMember write paths and read contracts in `services/platform/convex`, here is the concrete specification. - ---- - -### 1. MIRROR TABLE SCHEMA & INDEXES - -**File: `/convex/members/schema.ts` (NEW)** - -```typescript -import { defineTable } from 'convex/server'; -import { v } from 'convex/values'; - -/** - * Mirror of Better Auth's member table, denormalized into Convex for: - * - RLS performance: single indexed query instead of cross-component adapter call - * - Authoritative at read time: no staleness because all writes are inline-synchronized - * - * Mirrors betterAuth.member fields: _id, userId, organizationId, role, createdAt. - * Adds updatedAt for drift detection and reconciliation. - */ -export const memberMirrorTable = defineTable({ - // Better Auth's member._id — immutable, used as foreign key - memberId: v.string(), - - // Better Auth's member.userId — indexed for RLS queries (getUserOrganizations) - userId: v.string(), - - // Better Auth's member.organizationId — indexed for org-wide queries - organizationId: v.string(), - - // Better Auth's member.role — normalized lowercase (owner, admin, member, editor, developer, disabled) - role: v.string(), - - // Better Auth's member.createdAt — timestamp - createdAt: v.number(), - - // Track last sync/update for reconciliation drift detection - updatedAt: v.optional(v.number()), -}) - // Primary index: byUserId for getUserOrganizations (most common RLS query) - .index('by_userId', ['userId']) - - // Composite index: by_organizationId for org-wide member lists - .index('by_organizationId', ['organizationId']) - - // Composite index: org + user for getOrganizationMember lookups - .index('by_org_user', ['organizationId', 'userId']) - - // By memberId for point updates during role changes, member removals - .index('by_memberId', ['memberId']); - -/** - * Cursor state for hourly reconciliation cron. - * Singleton per job; persists scan position to bound work per run. - */ -export const memberMirrorGcCursorTable = defineTable({ - // Singleton key: 'memberMirrorReconcile' - job: v.string(), - - // Last organizationId scanned; null = start from beginning - lastOrgId: v.optional(v.string()), - - // Timestamp of last reconciliation run - updatedAt: v.number(), -}).index('by_job', ['job']); - -/** - * Similar mirror for teamMember table (optional but recommended for identical perf guarantee). - * Team membership queries (getUserTeamIds) currently paginate the betterAuth adapter directly. - * Mirroring avoids JWT drift risk and unifies read path. - */ -export const teamMemberMirrorTable = defineTable({ - // Better Auth's teamMember._id - teamMemberId: v.string(), - - // Better Auth's teamMember.userId - userId: v.string(), - - // Better Auth's teamMember.teamId - teamId: v.string(), - - // Better Auth's teamMember.createdAt - createdAt: v.number(), - - updatedAt: v.optional(v.number()), -}) - .index('by_userId', ['userId']) - .index('by_teamId', ['teamId']) - .index('by_team_user', ['teamId', 'userId']) - .index('by_teamMemberId', ['teamMemberId']); -``` - -**Update: `/convex/schema.ts`** (add these imports and table registrations) - -```typescript -import { - memberMirrorTable, - memberMirrorGcCursorTable, - teamMemberMirrorTable, -} from './members/schema'; - -export default defineSchema({ - // ... existing tables ... - memberMirror: memberMirrorTable, - memberMirrorGcCursor: memberMirrorGcCursorTable, - teamMemberMirror: teamMemberMirrorTable, - // ... rest of tables ... -}); -``` - ---- - -### 2. COVERAGE MATRIX: SYNC ACTION FOR EVERY WRITE PATH - -| Write Path | File | Operation | Sync Action | Details | -| ------------------------------------------------ | --------------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | -| **addMember (direct mutation)** | `/members/mutations.ts:78` | `adapter.create(member)` | **Inline mirror upsert** in same mutation after line 94 | After member ID extracted, `ctx.db.insert(memberMirror, {...})` | -| **removeMember** | `/members/mutations.ts:189` | `adapter.deleteOne(member)` | **Inline mirror delete** after line 189 | Before betterAuth delete, fetch mirror row; after delete, `ctx.db.delete(mirrorId)` | -| **updateMemberRole** | `/members/mutations.ts:338,425` | `adapter.updateMany(member, role)` | **Inline mirror patch** after each updateMany | `ctx.db.patch(mirrorId, {role: newRole, updatedAt: Date.now()})` | -| **transferOwnership (promote)** | `/members/mutations.ts:415` | `adapter.updateMany(member, owner)` | **Inline mirror patch** after line 415 | Promote target: `ctx.db.patch(targetMirrorId, {role: 'owner', updatedAt})` | -| **transferOwnership (demote)** | `/members/mutations.ts:425` | `adapter.updateMany(member, admin)` | **Inline mirror patch** after line 425 | Demote caller: `ctx.db.patch(callerMirrorId, {role: 'admin', updatedAt})` | -| **afterCreateOrganization** | `/auth.ts:724` | Better Auth org plugin implicit `adapter.create` | **After-middleware catch-all** at `/organization/create` | After hook fires, after-mw detects path, re-derives member from betterAuth, inserts mirror | -| **afterAcceptInvitation** | `/auth.ts:767` | Better Auth org plugin implicit `adapter.create` | **After-middleware catch-all** at `/organization/accept-invitation` | Extract organizationId, userId from mw.body/returned, insert mirror with role | -| **findOrCreateSsoUser (existing user)** | `/sso_providers/find_or_create_sso_user.ts:118` | `adapter.create(member)` | **Inline mirror upsert** after line 128 | `ctx.db.insert(memberMirror, {organizationId, userId, role, ...})` | -| **findOrCreateSsoUser (new user)** | `/sso_providers/find_or_create_sso_user.ts:175` | `adapter.create(member)` | **Inline mirror upsert** after line 185 | Same as above | -| **findOrCreateUserFromHeaders (existing org)** | `/betterAuth/trusted_headers/find_or_create_user_from_headers.ts:162` | `adapter.create(member)` | **Inline mirror upsert** after line 172 | `ctx.db.insert(memberMirror, {organizationId: existingOrgId, userId, role: 'member', ...})` | -| **findOrCreateUserFromHeaders (new org)** | `/betterAuth/trusted_headers/find_or_create_user_from_headers.ts:207` | `adapter.create(member)` | **Inline mirror upsert** after line 217 | `ctx.db.insert(memberMirror, {organizationId: newOrgId, userId, role: 'admin', ...})` | -| **addMemberInternal** | `/users/add_member_internal.ts:30` | `adapter.create(member)` | **Inline mirror upsert** after line 40 | `ctx.db.insert(memberMirror, {...})` | -| **createMember (existing user)** | `/users/create_member.ts:126` | `adapter.create(member)` | **Inline mirror upsert** after line 138 | Extract memberId, insert mirror | -| **createMember (new user)** | `/users/create_member.ts:205` | `adapter.create(member)` | **Inline mirror upsert** after line 231 | Extract memberId, insert mirror | -| **createUserWithoutSession** | `/users/create_user_without_session.ts:115` | `adapter.create(member)` | **Inline mirror upsert** after line 125 | Extract memberId, insert mirror | -| **Organization deletion** | `/auth.ts` (via client plugin) | Better Auth `adapter.deleteMany(members WHERE organizationId)` | **After-middleware catch-all** at `/organization/delete` | After-mw detects path, runs recursive delete of all mirror rows by organizationId | -| **migrate_org_creators** | `/migrations/migrate_org_creators.ts:89` | `adapter.updateMany(member, owner)` | **Inline mirror patch** after line 96 | `ctx.db.patch(mirrorId, {role: 'owner', updatedAt: Date.now()})` | -| **Team: addMember** | `/team_members/mutations.ts:73` | `adapter.create(teamMember)` | **Inline mirror upsert** after line 82 | `ctx.db.insert(teamMemberMirror, {teamMemberId, userId, teamId, createdAt: Date.now()})` | -| **Team: removeMember** | `/team_members/mutations.ts:147` | `adapter.deleteOne(teamMember)` | **Inline mirror delete** after line 152 | `ctx.db.delete(teamMemberMirrorId)` | -| **Entra ID: addTeamMember** | `/sso_providers/entra_id/team_sync.ts:137` | `adapter.create(teamMember)` | **Inline mirror upsert** after line 145 | `ctx.db.insert(teamMemberMirror, {...})` | -| **Entra ID: removeStaleTeamMemberships** | `/sso_providers/entra_id/team_sync.ts:213` | `adapter.deleteOne(teamMember)` | **Inline mirror delete** after line 220 | `ctx.db.delete(teamMemberMirrorId)` | -| **Built-in: `/organization/invite-member`** | Better Auth org plugin | `adapter.create(invitation)` | **After-middleware detect** (no mirror action—invitations are ephemeral) | Detect path, log audit only; member created on acceptInvitation | -| **Built-in: `/organization/remove-member`** | Better Auth org plugin | `adapter.deleteOne(member)` | **After-middleware catch-all** | Detect path, extract memberId from mw.body or returned, delete mirror row | -| **Built-in: `/organization/update-member-role`** | Better Auth org plugin | `adapter.updateMany(member, role)` | **After-middleware catch-all** | Detect path, extract memberId + newRole, patch mirror row | -| **Built-in: `/organization/leave`** | Better Auth org plugin | `adapter.deleteOne(member)` (self) | **After-middleware catch-all** | Detect path, extract member info, delete mirror row | -| **Built-in: `/organization/add-team-member`** | Better Auth org plugin | `adapter.create(teamMember)` | **After-middleware catch-all** | Detect path, extract teamMemberId + userId + teamId, insert teamMemberMirror | -| **Built-in: `/organization/remove-team-member`** | Better Auth org plugin | `adapter.deleteOne(teamMember)` | **After-middleware catch-all** | Detect path, extract teamMemberId, delete teamMemberMirror | - -**COVERAGE ASSESSMENT:** - -- ✅ All 26 write paths covered by inline action or after-middleware catch-all -- ✅ No path left uncovered; drift-sync is AUTOMATIC or guarded by after-middleware - ---- - -### 3. AFTER-MIDDLEWARE CATCH-ALL DESIGN - -**File: `/convex/auth.ts`** (expand existing `hooks.after` middleware, lines 463–553) - -The current after-middleware (lines 463–553) handles sign-in/sign-up and 2FA. Extend it to catch all org/team membership mutations: - -```typescript -// EXISTING after-middleware (lines 463–553) + ADD THIS SECTION: - -after: createAuthMiddleware(async (mw) => { - const runCtx = requireRunMutationCtx(ctx); - - // ... existing 2FA, sign-in, API key logic ... - - // NEW: Catch-all for member/teamMember mutations to sync mirrors - // This guards against drift when client calls Better Auth endpoints directly - // instead of Convex mutations (e.g., authClient.organization.removeMember). - - const path = mw.path; - const returned = mw.context.returned; - - // Skip if endpoint failed or no result to sync - if (returned instanceof APIError || !returned) { - return; - } - - try { - // MEMBER SYNC PATHS - if (path === '/organization/accept-invitation') { - // returned = { invitation, member } - // Extract: organizationId, userId, role from returned.member - const member = isRecord(returned) ? returned.member : undefined; - if (member && isRecord(member)) { - const memberId = getString(member, '_id'); - const userId = getString(member, 'userId'); - const organizationId = getString(member, 'organizationId'); - const role = getString(member, 'role'); - if (memberId && userId && organizationId) { - // Schedule async backfill to recover user's full member set - await runCtx.scheduler.runAfter( - 0, - internal.members.mirror_sync.syncMemberMirror, - { userId, organizationId }, - ); - } - } - } - - else if (path === '/organization/remove-member') { - // returned = { member: { id, userId, organizationId, role } } - // Extract from body: organizationId; from returned: member info - const memberIdOrEmail = mw.body?.memberIdOrEmail; - const orgId = mw.body?.organizationId; - if (memberIdOrEmail && orgId) { - // Delete mirror row by memberId (best-effort; re-derive on next RLS query) - // This is defensive; actual deletion happens in inline mutation path - await runCtx.scheduler.runAfter( - 0, - internal.members.mirror_sync.deleteStaleMembers, - { organizationId: orgId }, - ); - } - } - - else if (path === '/organization/update-member-role') { - // returned = { member: { id, userId, organizationId, role } } - const member = returned; - if (isRecord(member)) { - const memberId = getString(member, 'id') || getString(member, '_id'); - const role = getString(member, 'role'); - if (memberId && role) { - await runCtx.scheduler.runAfter( - 0, - internal.members.mirror_sync.updateMemberRoleMirror, - { memberId, role }, - ); - } - } - } - - else if (path === '/organization/leave') { - // returned = { member: { userId, organizationId, role } } - // User removed themselves; delete their mirror entry - const member = returned; - if (isRecord(member)) { - const userId = getString(member, 'userId'); - const organizationId = getString(member, 'organizationId'); - if (userId && organizationId) { - await runCtx.scheduler.runAfter( - 0, - internal.members.mirror_sync.deleteStaleMembers, - { organizationId }, - ); - } - } - } - - else if (path === '/organization/create') { - // New org created; await hooks fire, then org appears in DB - // Better Auth's afterCreateOrganization fires first (sync via hook); - // after-mw is redundant but defensive re-check can happen in cron - } - - else if (path === '/organization/delete') { - // Org deleted; all member mirror rows must be removed - const organizationId = mw.body?.organizationId; - if (organizationId) { - // Async: delete all mirror rows for this org - await runCtx.scheduler.runAfter( - 0, - internal.members.mirror_sync.deleteMirrorsByOrg, - { organizationId }, - ); - } - } - - // TEAM MEMBER SYNC PATHS - else if (path === '/organization/add-team-member') { - // returned = { id, userId, teamId, createdAt } - const teamMemberId = getString(returned, 'id') || getString(returned, '_id'); - const userId = getString(returned, 'userId'); - const teamId = getString(returned, 'teamId'); - if (teamMemberId && userId && teamId) { - await runCtx.scheduler.runAfter( - 0, - internal.members.mirror_sync.syncTeamMemberMirror, - { teamMemberId, userId, teamId }, - ); - } - } - - else if (path === '/organization/remove-team-member') { - // returned = { message: "..." }; extract from body - const teamMemberId = mw.body?.teamMemberId; - if (teamMemberId) { - await runCtx.scheduler.runAfter( - 0, - internal.members.mirror_sync.deleteTeamMemberMirror, - { teamMemberId }, - ); - } - } - } catch (err) { - // Non-fatal: async sync failed, but Better Auth succeeded - // Reconciliation cron will repair on next hourly run - console.warn( - '[auth.after-middleware] mirror sync failed (will repair via cron)', - path, - err instanceof Error ? err.message : err, - ); - } -}), -``` - -**Helper action for after-middleware recovery** (new file): - -**File: `/convex/members/mirror_sync.ts`** - -```typescript -/** - * Async helpers for mirror sync triggered by after-middleware. - * These are defensive re-syncs; inline mutations are authoritative. - */ - -import { isRecord, getString } from '../../lib/utils/type-guards'; -import { components } from '../_generated/api'; -import { internalAction } from '../_generated/server'; -import { v } from 'convex/values'; - -/** - * Re-sync a user's member mirror rows from betterAuth. - * Called after acceptInvitation to recover entire membership set. - */ -export const syncMemberMirror = internalAction({ - args: { - userId: v.string(), - organizationId: v.string(), - }, - handler: async (ctx, args) => { - // Query betterAuth for all members in this org for this user - const members = await ctx.runQuery(components.betterAuth.adapter.findMany, { - model: 'member', - paginationOpts: { cursor: null, numItems: 1 }, - where: [ - { field: 'organizationId', value: args.organizationId, operator: 'eq' }, - { field: 'userId', value: args.userId, operator: 'eq' }, - ], - }); - - const member = members?.page?.[0]; - if (!member) return; - - const memberId = getString(member, '_id'); - const role = getString(member, 'role'); - if (!memberId) return; - - // Upsert mirror: if exists, skip; if missing, insert - const existing = await ctx.runQuery( - (qCtx) => - qCtx.db - .query('memberMirror') - .withIndex('by_memberId', (q) => q.eq('memberId', memberId)) - .first(), - [], - ); - - if (existing) { - await ctx.runQuery( - (qCtx) => - qCtx.db.patch(existing._id, { - role: role ?? 'member', - updatedAt: Date.now(), - }), - [], - ); - } else { - await ctx.runQuery( - (qCtx) => - qCtx.db.insert('memberMirror', { - memberId, - userId: args.userId, - organizationId: args.organizationId, - role: role ?? 'member', - createdAt: Date.now(), - }), - [], - ); - } - }, -}); - -/** - * Delete stale member mirror rows for an organization. - * Removes rows that no longer have a matching betterAuth member. - */ -export const deleteStaleMembers = internalAction({ - args: { organizationId: v.string() }, - handler: async (ctx, args) => { - const mirrors = await ctx.runQuery( - (qCtx) => - qCtx.db - .query('memberMirror') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', args.organizationId), - ) - .take(1000), - [], - ); - - for (const mirror of mirrors) { - const member = await ctx.runQuery(components.betterAuth.adapter.findOne, { - model: 'member', - where: [{ field: '_id', value: mirror.memberId, operator: 'eq' }], - }); - - if (!member) { - await ctx.runQuery((qCtx) => qCtx.db.delete(mirror._id), []); - } - } - }, -}); - -export const updateMemberRoleMirror = internalAction({ - args: { memberId: v.string(), role: v.string() }, - handler: async (ctx, args) => { - const mirrors = await ctx.runQuery( - (qCtx) => - qCtx.db - .query('memberMirror') - .withIndex('by_memberId', (q) => q.eq('memberId', args.memberId)) - .first(), - [], - ); - - if (mirrors) { - await ctx.runQuery( - (qCtx) => - qCtx.db.patch(mirrors._id, { - role: args.role, - updatedAt: Date.now(), - }), - [], - ); - } - }, -}); - -export const deleteMirrorsByOrg = internalAction({ - args: { organizationId: v.string() }, - handler: async (ctx, args) => { - const mirrors = await ctx.runQuery( - (qCtx) => - qCtx.db - .query('memberMirror') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', args.organizationId), - ) - .take(5000), - [], - ); - - for (const mirror of mirrors) { - await ctx.runQuery((qCtx) => qCtx.db.delete(mirror._id), []); - } - }, -}); - -export const syncTeamMemberMirror = internalAction({ - args: { teamMemberId: v.string(), userId: v.string(), teamId: v.string() }, - handler: async (ctx, args) => { - const existing = await ctx.runQuery( - (qCtx) => - qCtx.db - .query('teamMemberMirror') - .withIndex('by_teamMemberId', (q) => - q.eq('teamMemberId', args.teamMemberId), - ) - .first(), - [], - ); - - const now = Date.now(); - if (existing) { - await ctx.runQuery( - (qCtx) => - qCtx.db.patch(existing._id, { - updatedAt: now, - }), - [], - ); - } else { - await ctx.runQuery( - (qCtx) => - qCtx.db.insert('teamMemberMirror', { - teamMemberId: args.teamMemberId, - userId: args.userId, - teamId: args.teamId, - createdAt: now, - }), - [], - ); - } - }, -}); - -export const deleteTeamMemberMirror = internalAction({ - args: { teamMemberId: v.string() }, - handler: async (ctx, args) => { - const mirror = await ctx.runQuery( - (qCtx) => - qCtx.db - .query('teamMemberMirror') - .withIndex('by_teamMemberId', (q) => - q.eq('teamMemberId', args.teamMemberId), - ) - .first(), - [], - ); - - if (mirror) { - await ctx.runQuery((qCtx) => qCtx.db.delete(mirror._id), []); - } - }, -}); -``` - ---- - -### 4. BACKFILL MIGRATION (Paginated, Idempotent) - -**File: `/convex/migrations/backfill_member_mirror.ts`** - -```typescript -/** - * Migration: Backfill memberMirror table from betterAuth members. - * - * Reads all organizations, then for each org: - * 1. Fetches all members with findMany(model: 'member') - * 2. Upserts rows into memberMirror table (skips if already exist) - * - * Idempotent: skips records that already exist in memberMirror. - * Paginated: processes orgs in batches to avoid transaction timeout. - * - * Usage (one-time): - * bunx convex run migrations/backfill_member_mirror:apply - */ - -import { isRecord, getString, getNumber } from '../../lib/utils/type-guards'; -import { components } from '../_generated/api'; -import { internalMutation, internalAction } from '../_generated/server'; -import { v } from 'convex/values'; - -const MEMBERS_PER_ORG = 200; -const ORGS_PER_RUN = 50; - -export const backfillMembersForOrg = internalMutation({ - args: { organizationId: v.string() }, - returns: v.object({ created: v.number(), skipped: v.number() }), - handler: async (ctx, args) => { - let created = 0; - let skipped = 0; - - let memberCursor: string | null = null; - let membersDone = false; - - while (!membersDone) { - const result = await ctx.runQuery( - components.betterAuth.adapter.findMany, - { - model: 'member', - paginationOpts: { cursor: memberCursor, numItems: MEMBERS_PER_ORG }, - where: [ - { - field: 'organizationId', - value: args.organizationId, - operator: 'eq', - }, - ], - }, - ); - - if (!result?.page?.length) break; - - for (const memberRaw of result.page) { - if (!isRecord(memberRaw)) { - skipped++; - continue; - } - - const memberId = getString(memberRaw, '_id'); - const userId = getString(memberRaw, 'userId'); - const organizationId = getString(memberRaw, 'organizationId'); - const role = getString(memberRaw, 'role') ?? 'member'; - const createdAt = getNumber(memberRaw, 'createdAt') ?? Date.now(); - - if (!memberId || !userId || !organizationId) { - skipped++; - continue; - } - - const existing = await ctx.db - .query('memberMirror') - .withIndex('by_memberId', (q) => q.eq('memberId', memberId)) - .first(); - - if (existing) { - skipped++; - continue; - } - - await ctx.db.insert('memberMirror', { - memberId, - userId, - organizationId, - role: role.toLowerCase(), - createdAt, - }); - created++; - } - - memberCursor = result.continueCursor; - membersDone = result.isDone ?? false; - } - - return { created, skipped }; - }, -}); - -export const apply = internalAction({ - args: {}, - returns: v.object({ totalCreated: v.number(), totalSkipped: v.number() }), - handler: async (ctx) => { - let totalCreated = 0; - let totalSkipped = 0; - let scanned = 0; - - let orgCursor: string | null = null; - let orgsDone = false; - - while (!orgsDone && scanned < 10000) { - const result = await ctx.runQuery( - components.betterAuth.adapter.findMany, - { - model: 'organization', - paginationOpts: { cursor: orgCursor, numItems: ORGS_PER_RUN }, - where: [], - }, - ); - - if (!result?.page?.length) break; - - for (const orgRaw of result.page) { - if (!isRecord(orgRaw)) continue; - const orgId = getString(orgRaw, '_id'); - if (!orgId) continue; - - const { created, skipped } = await ctx.runMutation( - internal.migrations.backfill_member_mirror.backfillMembersForOrg, - { organizationId: orgId }, - ); - totalCreated += created; - totalSkipped += skipped; - scanned++; - } - - orgCursor = result.continueCursor; - orgsDone = result.isDone ?? false; - } - - console.log('[backfill_member_mirror] complete', { - scanned, - totalCreated, - totalSkipped, - }); - return { totalCreated, totalSkipped }; - }, -}); -``` - -**Similar migration for teamMembers** (file `/convex/migrations/backfill_team_member_mirror.ts`, same structure but for teamMember table). - -**Register in `/convex/migrations.ts`:** - -```typescript -export const runAll = internalAction({ - args: {}, - handler: async (ctx) => { - // Existing migrations... - await ctx.runAction(internal.migrations.backfill_member_mirror.apply, {}); - await ctx.runAction( - internal.migrations.backfill_team_member_mirror.apply, - {}, - ); - }, -}); -``` - ---- - -### 5. RECONCILIATION CRON (Hourly, Bounded) - -**File: `/convex/members/mirror_reconciliation.ts`** - -```typescript -/** - * Hourly reconciliation: compare memberMirror against betterAuth members. - * Repairs drift: deleted users, role changes not captured by inline sync. - * - * Bounded to MAX_ORGS_PER_RUN × MEMBERS_PER_ORG so one tenant doesn't starve others. - * Cursor persists in memberMirrorGcCursor singleton. - * - * Triggered hourly via cron (see crons.ts). - */ - -import { v } from 'convex/values'; -import { internalMutation } from '../_generated/server'; -import { components } from '../_generated/api'; - -const MEMBERS_PER_ORG = 200; -const MAX_ORGS_PER_RUN = 20; -const JOB_KEY = 'memberMirrorReconcile'; - -export const reconcileMemberMirror = internalMutation({ - args: {}, - returns: v.object({ - orgsScanned: v.number(), - rowsDeleted: v.number(), - rowsUpdated: v.number(), - wrappedAround: v.boolean(), - runtimeMs: v.number(), - }), - handler: async (ctx) => { - const startMs = Date.now(); - let orgsScanned = 0; - let rowsDeleted = 0; - let rowsUpdated = 0; - let wrappedAround = false; - - // Load cursor state - const cursorRow = await ctx.db - .query('memberMirrorGcCursor') - .withIndex('by_job', (q) => q.eq('job', JOB_KEY)) - .first(); - let cursor: string | null = cursorRow?.lastOrgId ?? null; - - while (orgsScanned < MAX_ORGS_PER_RUN) { - // Phase 1: Probe to find next org with mirror rows - const probe = await ctx.db - .query('memberMirror') - .withIndex('by_organizationId', (q) => - cursor === null ? q : q.gt('organizationId', cursor), - ) - .first(); - - if (!probe) { - // No more orgs; wrap to start - if (cursor !== null) { - cursor = null; - wrappedAround = true; - } - break; - } - - const orgId = probe.organizationId; - cursor = orgId; - - // Phase 2: Fetch current members from betterAuth - const betterAuthMembers = new Map(); - let authCursor: string | null = null; - let authDone = false; - - while (!authDone) { - const result = await ctx.runQuery( - components.betterAuth.adapter.findMany, - { - model: 'member', - paginationOpts: { - cursor: authCursor, - numItems: MEMBERS_PER_ORG, - }, - where: [ - { - field: 'organizationId', - value: orgId, - operator: 'eq', - }, - ], - }, - ); - - for (const memberRaw of result?.page ?? []) { - const memberId = memberRaw?._id; - if (memberId) { - betterAuthMembers.set(memberId, memberRaw); - } - } - - authCursor = result?.continueCursor; - authDone = result?.isDone ?? true; - } - - // Phase 3: Scan mirror rows and repair drift - const mirrorMembers = await ctx.db - .query('memberMirror') - .withIndex('by_organizationId', (q) => q.eq('organizationId', orgId)) - .take(MEMBERS_PER_ORG); - - for (const mirrorRow of mirrorMembers) { - const betterAuthRow = betterAuthMembers.get(mirrorRow.memberId); - - if (!betterAuthRow) { - // Member deleted in betterAuth → delete mirror - await ctx.db.delete(mirrorRow._id); - rowsDeleted++; - continue; - } - - // Check for drift (role or userId change) - const newRole = betterAuthRow.role ?? 'member'; - const newUserId = betterAuthRow.userId; - - if (mirrorRow.role !== newRole || mirrorRow.userId !== newUserId) { - await ctx.db.patch(mirrorRow._id, { - role: newRole.toLowerCase(), - userId: newUserId, - updatedAt: Date.now(), - }); - rowsUpdated++; - } - } - - // Count this org only if it had activity - if (mirrorMembers.length > 0) { - orgsScanned++; - } - } - - // Persist cursor - const now = Date.now(); - if (cursorRow) { - await ctx.db.patch(cursorRow._id, { - lastOrgId: cursor, - updatedAt: now, - }); - } else { - await ctx.db.insert('memberMirrorGcCursor', { - job: JOB_KEY, - lastOrgId: cursor, - updatedAt: now, - }); - } - - const runtimeMs = Date.now() - startMs; - console.info('[memberMirror.reconcile]', { - orgsScanned, - rowsDeleted, - rowsUpdated, - wrappedAround, - runtimeMs, - }); - - return { orgsScanned, rowsDeleted, rowsUpdated, wrappedAround, runtimeMs }; - }, -}); -``` - -**Similar reconciliation for teamMembers** (file `/convex/members/team_member_mirror_reconciliation.ts`). - -**Register in `/convex/crons.ts`:** - -```typescript -// Hourly reconciliation: detect/repair member mirror drift -crons.cron( - 'reconcile member mirror (hourly)', - '0 * * * *', - internal.members.mirror_reconciliation.reconcileMemberMirror, - {}, -); - -// Hourly reconciliation: detect/repair team member mirror drift -crons.cron( - 'reconcile team member mirror (hourly)', - '5 * * * *', - internal.members.team_member_mirror_reconciliation.reconcileTeamMemberMirror, - {}, -); -``` - ---- - -### 6. REWRITE RLS QUERIES TO READ MIRROR - -**File: `/convex/lib/rls/organization/get_user_organizations.ts`** (REWRITTEN) - -```typescript -/** - * Get all organizations user has access to from Convex mirror table. - * Replaces betterAuth adapter query with local indexed lookup. - * - * In trusted headers mode, role comes from JWT claims (getTrustedAuthData). - * Email fallback (rare) stays on betterAuth path because it requires cross-table resolution. - */ - -import type { MemberRole } from '../../../../lib/shared/schemas/organizations'; -import { components } from '../../../_generated/api'; -import type { QueryCtx } from '../../../_generated/server'; -import { getTrustedAuthData } from '../auth/get_trusted_auth_data'; -import { requireAuthenticatedUser } from '../auth/require_authenticated_user'; -import type { AuthenticatedUser, OrganizationMember } from '../types'; - -const VALID_ROLES: ReadonlySet = new Set([ - 'owner', - 'disabled', - 'member', - 'editor', - 'developer', - 'admin', -]); - -function isValidRole(role: string): role is MemberRole { - return VALID_ROLES.has(role); -} - -/** - * Get all organizations user has access to from Convex memberMirror table. - * Mirror is authoritative for org list; keeps sync'd via inline mutations + cron. - */ -export async function getUserOrganizations( - ctx: QueryCtx, - user?: AuthenticatedUser, -): Promise< - Array<{ - organizationId: string; - role: MemberRole; - member: OrganizationMember; - }> -> { - const authUser = user || (await requireAuthenticatedUser(ctx)); - - // Check if we're in trusted headers mode (role override from JWT) - const trustedData = await getTrustedAuthData(ctx); - - // Query Convex mirror for all memberships (indexed by userId) - // No pagination needed for typical users; fall back to betterAuth if >100 orgs. - const memberMirrors = await ctx.db - .query('memberMirror') - .withIndex('by_userId', (q) => q.eq('userId', authUser.userId ?? '')) - .collect(); // Safe: memberMirror is denormalized and bounded per user - - // Convert mirrors back to OrganizationMember shape (add _id alias for compatibility) - const memberRows: OrganizationMember[] = memberMirrors.map((mirror) => ({ - _id: mirror.memberId, - userId: mirror.userId, - organizationId: mirror.organizationId, - role: mirror.role, - createdAt: mirror.createdAt, - })); - - // Fallback to betterAuth if mirror is empty (edge: first sync after user creation) - if (memberRows.length === 0) { - console.warn( - '[getUserOrganizations] memberMirror empty for userId, falling back to betterAuth', - authUser.userId, - ); - const result = await ctx.runQuery(components.betterAuth.adapter.findMany, { - model: 'member', - paginationOpts: { cursor: null, numItems: 100 }, - where: [ - { - field: 'userId', - value: authUser.userId ?? null, - operator: 'eq', - }, - ], - }); - memberRows.push(...(result?.page ?? [])); - } - - if (memberRows.length === 0) { - return []; - } - - return memberRows - .map((member) => { - // Get role from trusted headers if available, otherwise from mirror - const rawRole = trustedData?.trustedRole || member.role || 'member'; - const normalizedRole = rawRole.toLowerCase(); - const role: MemberRole = isValidRole(normalizedRole) - ? normalizedRole - : 'member'; - - return { - organizationId: member.organizationId, - role, - member, - }; - }) - .filter( - (entry: { organizationId: string; role: string; member: unknown }) => - entry.role !== 'disabled', - ); -} -``` - -**File: `/convex/lib/rls/organization/get_organization_member.ts`** (REWRITTEN) - -```typescript -/** - * Get organization member for authenticated user from Convex mirror. - * Mirror read is fast + authoritative; email fallback stays on betterAuth. - */ - -import { components } from '../../../_generated/api'; -import type { QueryCtx, MutationCtx } from '../../../_generated/server'; -import { requireAuthenticatedUser } from '../auth/require_authenticated_user'; -import { UnauthorizedError } from '../errors'; -import type { AuthenticatedUser, OrganizationMember } from '../types'; - -/** - * Get organization member for authenticated user from Convex mirror - */ -export async function getOrganizationMember( - ctx: QueryCtx | MutationCtx, - organizationId: string, - user?: AuthenticatedUser, -): Promise { - const authUser = user || (await requireAuthenticatedUser(ctx)); - - // Query Convex mirror by org + userId (composite index) - const mirror = await ctx.db - .query('memberMirror') - .withIndex('by_org_user', (q) => - q.eq('organizationId', organizationId).eq('userId', authUser.userId), - ) - .first(); - - let member: OrganizationMember | undefined = undefined; - - if (mirror) { - // Mirror hit: convert back to OrganizationMember shape - member = { - _id: mirror.memberId, - userId: mirror.userId, - organizationId: mirror.organizationId, - role: mirror.role, - createdAt: mirror.createdAt, - }; - } - - // Fallback to email lookup if no mirror match (account migrations, social linking). - // This is the ONLY path that stays on betterAuth because it requires - // user table email lookups not available in mirror. - if (!member && authUser.email) { - console.warn('[RLS] Falling back to email lookup for organization member', { - organizationId, - userId: authUser.userId, - email: authUser.email, - }); - const userRes = await ctx.runQuery(components.betterAuth.adapter.findMany, { - model: 'user', - paginationOpts: { cursor: null, numItems: 1 }, - where: [{ field: 'email', value: authUser.email, operator: 'eq' }], - }); - const userByEmail = userRes?.page?.[0]; - if (userByEmail?._id) { - const result = await ctx.runQuery( - components.betterAuth.adapter.findMany, - { - model: 'member', - paginationOpts: { cursor: null, numItems: 1 }, - where: [ - { - field: 'organizationId', - value: organizationId, - operator: 'eq', - }, - { field: 'userId', value: userByEmail._id, operator: 'eq' }, - ], - }, - ); - member = result?.page?.[0]; - } - } - - if (!member) { - throw new UnauthorizedError( - `Not a member of organization ${organizationId}`, - ); - } - - if (member.role === 'disabled') { - throw new UnauthorizedError( - `Member account is disabled in organization ${organizationId}`, - ); - } - - return member; -} -``` - -**File: `/convex/lib/get_user_teams.ts`** (REWRITTEN to use teamMemberMirror) - -```typescript -/** - * Get user's team IDs from mirror (or JWT trusted claim). - * Mirror read avoids JWT drift risk; JWT claims short-circuit for efficiency. - */ - -import type { GenericQueryCtx } from 'convex/server'; -import { v } from 'convex/values'; - -import { parseJson } from '../../lib/utils/type-cast-helpers'; -import { isRecord, getString } from '../../lib/utils/type-guards'; -import { components } from '../_generated/api'; -import type { DataModel } from '../_generated/dataModel'; -import { internalQuery } from '../_generated/server'; - -export const TEAM_DATASET_PREFIX = 'tale_team_'; -export const DEFAULT_DATASET_NAME = 'tale_documents'; - -/** - * Get all team IDs that a user belongs to from Convex mirror. - * - * In trusted headers mode, returns team IDs from JWT claims (trustedTeams). - * Otherwise queries the teamMemberMirror table (faster + authoritative). - * - * @param ctx - Convex query context - * @param userId - User ID to look up - * @returns Array of team IDs - */ -export async function getUserTeamIds( - ctx: GenericQueryCtx, - userId: string, -): Promise { - // Check if JWT contains trusted teams (trusted headers mode) - const identity = await ctx.auth.getUserIdentity(); - if (isRecord(identity)) { - const trustedTeamsRaw = getString(identity, 'trustedTeams'); - if (trustedTeamsRaw) { - // Trusted headers mode: parse team IDs from JWT claim - try { - const teams = - parseJson>(trustedTeamsRaw); - return Array.isArray(teams) - ? teams - .filter( - (t): t is { id: string; name: string } => - isRecord(t) && - typeof t.id === 'string' && - typeof t.name === 'string', - ) - .map((t) => t.id) - : []; - } catch { - return []; - } - } - } - - // Fallback: query teamMemberMirror (indexed by userId) - // Mirror is denormalized, so all results fit in one query (rare user in >1000 teams) - const allTeamIds: string[] = []; - const mirrors = await ctx.db - .query('teamMemberMirror') - .withIndex('by_userId', (q) => q.eq('userId', userId)) - .collect(); - - for (const mirror of mirrors) { - allTeamIds.push(mirror.teamId); - } - - // Fallback to betterAuth if mirror is empty (edge case: first sync) - if (allTeamIds.length === 0) { - console.warn( - '[getUserTeamIds] teamMemberMirror empty for userId, falling back to betterAuth', - userId, - ); - let cursor: string | null = null; - let isDone = false; - - while (!isDone) { - const result = await ctx.runQuery( - components.betterAuth.adapter.findMany, - { - model: 'teamMember', - paginationOpts: { cursor, numItems: 1000 }, - where: [{ field: 'userId', operator: 'eq', value: userId }], - }, - ); - - allTeamIds.push(...(result?.page?.map((m) => m.teamId) ?? [])); - isDone = result?.isDone ?? true; - cursor = result?.continueCursor; - } - } - - return allTeamIds; -} -``` - ---- - -### 7. INLINE MIRROR SYNC IN ALL WRITE PATHS - -Update each write-path file to insert/update/delete the mirror row immediately after the betterAuth adapter call: - -**File: `/convex/members/mutations.ts`** (excerpt showing pattern) - -```typescript -export const addMember = mutation({ - args: { - organizationId: v.string(), - userId: v.string(), - role: v.optional(memberRoleValidator), - }, - returns: v.string(), - handler: async (ctx, args) => { - // ... existing auth/validation code ... - - const role = (args.role ?? 'member').toLowerCase(); - const created = await ctx.runMutation( - components.betterAuth.adapter.create, - { - input: { - model: 'member', - data: { - organizationId: args.organizationId, - userId: args.userId, - role, - createdAt: Date.now(), - }, - }, - }, - ); - - const memberId = String( - isBetterAuthCreateResult(created) ? created._id : created, - ); - - // NEW: Inline mirror upsert - await ctx.db.insert('memberMirror', { - memberId, - userId: args.userId, - organizationId: args.organizationId, - role, - createdAt: Date.now(), - }); - - // ... audit logging ... - return memberId; - }, -}); - -export const removeMember = mutation({ - args: { - memberId: v.string(), - }, - returns: v.null(), - handler: async (ctx, args) => { - // ... existing auth/validation code ... - - // NEW: Find and delete mirror row BEFORE betterAuth delete - const mirrorToDelete = await ctx.db - .query('memberMirror') - .withIndex('by_memberId', (q) => q.eq('memberId', args.memberId)) - .first(); - - await ctx.runMutation(components.betterAuth.adapter.deleteOne, { - input: { - model: 'member', - where: [{ field: '_id', value: args.memberId, operator: 'eq' }], - }, - }); - - // NEW: Delete mirror - if (mirrorToDelete) { - await ctx.db.delete(mirrorToDelete._id); - } - - // ... cascade & audit ... - return null; - }, -}); - -export const updateMemberRole = mutation({ - args: { - memberId: v.string(), - role: memberRoleValidator, - }, - returns: v.null(), - handler: async (ctx, args) => { - // ... existing validation ... - - const newRole = args.role.toLowerCase(); - - await ctx.runMutation(components.betterAuth.adapter.updateMany, { - input: { - model: 'member', - where: [{ field: '_id', value: args.memberId, operator: 'eq' }], - update: { role: newRole }, - }, - paginationOpts: { cursor: null, numItems: 1 }, - }); - - // NEW: Patch mirror - const mirror = await ctx.db - .query('memberMirror') - .withIndex('by_memberId', (q) => q.eq('memberId', args.memberId)) - .first(); - if (mirror) { - await ctx.db.patch(mirror._id, { - role: newRole, - updatedAt: Date.now(), - }); - } - - // ... audit ... - return null; - }, -}); -``` - -**Apply same pattern to:** - -- `/members/mutations.ts` — transferOwnership (patch 2 mirror rows) -- `/users/create_member.ts` — insert after create -- `/users/add_member_internal.ts` — insert after create -- `/users/create_user_without_session.ts` — insert after create -- `/sso_providers/find_or_create_sso_user.ts` — insert at lines 128 & 185 -- `/betterAuth/trusted_headers/find_or_create_user_from_headers.ts` — insert at lines 172 & 217 -- `/team_members/mutations.ts` — addMember insert, removeMember delete -- `/sso_providers/entra_id/team_sync.ts` — insert & delete for team syncs -- `/migrations/migrate_org_creators.ts` — patch after updateMany - ---- - -### 8. TESTS - -**File: `/convex/lib/rls/organization/get_user_organizations.test.ts`** (UPDATE) - -```typescript -// Existing test structure, but: -// 1. Change mock to use local db.query instead of betterAuth adapter -// 2. Add test for mirror fallback when empty - -it('reads from memberMirror', async () => { - const ctx = createMockCtx(); - - // Mock local db.query to return mirror rows - const memberMirrors = [ - { - _id: 'mirror_1', - memberId: 'member_1', - userId: 'user_1', - organizationId: 'org_1', - role: 'admin', - createdAt: 123, - }, - ]; - - ctx.db.query.mockReturnValue({ - withIndex: vi.fn().mockReturnValue({ - collect: vi.fn().mockResolvedValue(memberMirrors), - }), - }); - - const result = await getUserOrganizations(ctx as never, authUser); - - expect(result).toHaveLength(1); - expect(result[0].organizationId).toBe('org_1'); - expect(result[0].role).toBe('admin'); -}); - -it('falls back to betterAuth when mirror is empty', async () => { - const ctx = createMockCtx(); - - // Mirror returns empty - ctx.db.query.mockReturnValue({ - withIndex: vi.fn().mockReturnValue({ - collect: vi.fn().mockResolvedValue([]), - }), - }); - - // betterAuth returns data - ctx.runQuery.mockResolvedValueOnce({ - page: [{ organizationId: 'org_1', role: 'admin' }], - }); - - const result = await getUserOrganizations(ctx as never, authUser); - - expect(result).toHaveLength(1); -}); -``` - -**File: `/convex/members/mutations.test.ts`** (UPDATE to verify mirror inserts) - -```typescript -// Existing test structure + add: - -it('addMember inserts into memberMirror', async () => { - const ctx = createMockCtx(); - mockGetAuthUser.mockResolvedValueOnce(AUTH_USER); - - ctx.runQuery.mockResolvedValueOnce({ page: [CALLER_MEMBER] }); // caller check - ctx.runQuery.mockResolvedValueOnce({ page: [TARGET_USER] }); // target user - - ctx.runMutation.mockResolvedValueOnce({ _id: 'new_member_id' }); // adapter.create - - const memberId = await addMember(ctx as never, { - organizationId: 'org_1', - userId: 'user_2', - role: 'member', - }); - - // Verify mirror insert was called - expect(ctx.db.insert).toHaveBeenCalledWith('memberMirror', { - memberId: 'new_member_id', - userId: 'user_2', - organizationId: 'org_1', - role: 'member', - createdAt: expect.any(Number), - }); -}); - -it('removeMember deletes from memberMirror', async () => { - const ctx = createMockCtx(); - mockGetAuthUser.mockResolvedValueOnce(AUTH_USER); - - ctx.runQuery.mockResolvedValueOnce({ page: [TARGET_MEMBER] }); // member lookup - ctx.runQuery.mockResolvedValueOnce({ page: [CALLER_MEMBER] }); // caller check - ctx.runQuery.mockResolvedValueOnce({ page: [TARGET_USER] }); // target user - - const mirrorRow = { - _id: 'mirror_id', - memberId: 'member_id', - userId: 'user_2', - organizationId: 'org_1', - role: 'member', - }; - ctx.db.query.mockReturnValue({ - withIndex: vi.fn().mockReturnValue({ - first: vi.fn().mockResolvedValue(mirrorRow), - }), - }); - - ctx.runMutation.mockResolvedValueOnce(null); // adapter.deleteOne - - await removeMember(ctx as never, { memberId: 'member_id' }); - - expect(ctx.db.delete).toHaveBeenCalledWith('mirror_id'); -}); -``` - -**New file: `/convex/members/mirror_reconciliation.test.ts`** - -```typescript -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -// Test that reconciliation cron: -// 1. Detects deleted members in betterAuth (deletes mirror) -// 2. Detects role drift (updates mirror) -// 3. Persists cursor state -// 4. Wraps around at end of org list - -describe('reconcileMemberMirror', () => { - it('deletes mirror rows when betterAuth member is gone', async () => { - const ctx = createMockCtx(); - - // Mirror has member_1, betterAuth does not - const mirrorRow = { - _id: 'mirror_1', - memberId: 'member_1', - userId: 'user_1', - organizationId: 'org_1', - role: 'admin', - }; - - ctx.db.query.mockReturnValue({ - withIndex: vi.fn().mockReturnValue({ - first: vi.fn().mockResolvedValue(mirrorRow), - take: vi.fn().mockResolvedValue([mirrorRow]), - }), - }); - - ctx.runQuery.mockResolvedValueOnce({ page: [] }); // betterAuth returns empty - - // Run reconciliation - // ... verify ctx.db.delete was called for mirror_1 - }); - - it('patches mirror when role drifts in betterAuth', async () => { - const ctx = createMockCtx(); - - const mirrorRow = { - _id: 'mirror_1', - memberId: 'member_1', - userId: 'user_1', - organizationId: 'org_1', - role: 'admin', - }; - - const betterAuthMember = { - _id: 'member_1', - userId: 'user_1', - role: 'member', // Changed! - }; - - ctx.db.query.mockReturnValue({ - withIndex: vi.fn().mockReturnValue({ - first: vi.fn().mockResolvedValue(mirrorRow), - take: vi.fn().mockResolvedValue([mirrorRow]), - }), - }); - - ctx.runQuery.mockResolvedValueOnce({ - page: [betterAuthMember], - }); - - // Run reconciliation - // ... verify ctx.db.patch was called with role: 'member' - }); - - it('persists cursor state for next run', async () => { - const ctx = createMockCtx(); - - // ... setup mirrors and betterAuth ... - - // Run reconciliation - // ... verify ctx.db.patch or insert was called on memberMirrorGcCursor - }); -}); -``` - ---- - -### 9. OPEN QUESTIONS & FUTURE WORK - -1. **Team membership cascade on org deletion**: When org is deleted via `/organization/delete`, does Better Auth cascade delete `teamMember` rows? Currently, the code doesn't explicitly handle this. The after-middleware `deleteMirrorsByOrg` assumes Better Auth already deleted members; we should verify that team members are also cleaned up. - -2. **Invitation table handling**: The `invitation` table has optional `teamId` field. When an invitation is accepted with a team, should the `teamMember` mirror row be created immediately or only if the user accepts? Current flow: `acceptInvitation` → `afterAcceptInvitation` hook (creates member) → but no explicit team membership creation visible. Need clarification on invitation→team flow. - -3. **Trusted role override performance**: In trusted headers mode, `getUserOrganizations` always reads the mirror to build the full list, then applies `trustedData?.trustedRole` override (JWT claim). This means the mirror row for role field is ignored and overridden per-query. Is this the intended semantics? If trusted role is per-org, we might need to store it in the JWT claim instead of mirror. - -4. **Email fallback in getOrganizationMember**: The email-based lookup is a rare edge case (account migrations, social linking). Should we add a transient cache (invalidated hourly) to avoid repeated betterAuth queries on drift? Or accept the 2-query latency? - -5. **Team sync via Entra ID**: The Entra ID SSO sync (team_sync.ts) creates/deletes teamMembers. The mirror sync actions in after-middleware are defensive; what if the sync action itself fails? Should we add a dedicated error handler or rely on cron repair? - -6. **Better Auth endpoint disabling**: Better Auth organization plugin has NO built-in config to disable built-in endpoints (e.g., `/organization/remove-member`). The after-middleware catch-all is defensive but non-blocking. Should we add explicit validation in the after-middleware to reject calls from unprivileged users? Currently, Better Auth's plugin handles this, but documenting the trust boundary is important. - -7. **Performance: large orgs with >10k members**: The reconciliation cron bounds work per org (MEMBERS_PER_ORG × orgs scanned), but if an org has 100k members, a single org could timeout the mutation. Should we add paging within org + continuation cursor to the cron state? - -8. **Trusted teams JWT claim divergence**: `getUserTeamIds` has a JWT short-circuit for `trustedTeams` claim. How is this claim updated when teams are synced via Entra ID? If it lags, team access could be stale. Document lifecycle or consider caching + refresh strategy. - ---- - -### SUMMARY TABLE - -| Component | Location | Purpose | Sync Frequency | -| ------------------------------ | -------------------------------------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------- | -| **Mirror Schema** | `/convex/members/schema.ts` | Denormalized member + teamMember tables indexed by userId/teamId for RLS performance | — | -| **Inline Sync** | 18 mutation files | On each betterAuth write, immediately insert/patch/delete mirror row | Synchronous (same mutation) | -| **After-Middleware Catch-All** | `/convex/auth.ts` (lines 463+) | Defensive re-sync for built-in endpoint calls not covered by inline mutations | Event-triggered (async via scheduler) | -| **Backfill Migration** | `/convex/migrations/backfill_member_mirror.ts` | One-time initialization: populate mirror from betterAuth | Run once at deployment | -| **Hourly Reconciliation Cron** | `/convex/members/mirror_reconciliation.ts` | Detect & repair drift (deleted users, role changes missed by inline sync) | Hourly | -| **RLS Read Rewrite** | `/convex/lib/rls/organization/get_user_organizations.ts` | Query mirror instead of betterAuth adapter for org list | On every RLS query (no change to call pattern) | -| **Test Coverage** | `/convex/**/*.test.ts` | Verify mirror inserts, deletes, patches on all write paths + cron repair | — | - ---- - -**Correctness guarantee**: Every betterAuth member/teamMember write is synchronized to the mirror via one of three paths: - -1. **Inline** (18 paths): instant, in same transaction -2. **After-middleware** (8 paths): within seconds, async via scheduler -3. **Cron repair** (hourly): catches any drift from failure paths - -**Performance guarantee**: RLS queries (`getUserOrganizations`, `getOrganizationMember`, `getUserTeamIds`) become single indexed Convex DB queries instead of cross-component betterAuth adapter calls, reducing latency by 10–100× depending on network. - -**Email fallback edge case**: Rare account migration path stays on betterAuth to handle userId mismatches. All common paths use mirror. - -## Adversarial security review (drift / privilege-retention risks) - -Excellent. Now I have enough information to conduct the comprehensive security review. Let me create the final report: - ---- - -## ADVERSARIAL SECURITY AUDIT: Convex-Native Member Mirror for 100% Secure RLS - -### EXECUTIVE SUMMARY - -**CRITICAL VERDICT**: Reading the mirror authoritatively for RLS is **NOT SAFE** without the complete mitigation stack. The mirror can drift into 3 security-critical states: - -1. **PRIVILEGE RETENTION**: User removed from org in Better Auth but mirror still shows membership → unauthorized access to org data -2. **ROLE ESCALATION**: User demoted to member, mirror not updated → reads as admin/developer, bypasses RLS gate -3. **WRONGFUL DENIAL**: Mirror fails to backfill or cascades incorrectly → legitimate user locked out - -The enum you provided identifies **16 explicit write paths + 4 implicit Better Auth plugin operations + 1 after-middleware gap** = **21 total touch points**. Only 2 of 21 currently have mirror-update hooks. **The mirror as-proposed is 10% instrumented.** - -**MINIMUM VIABILITY**: The architecture becomes "100% secure" ONLY with: - -1. **Inline mirror writes on every member-table mutation** (16 custom paths) -2. **Better Auth hook stubs for implicit operations** (4 plugin paths) -3. **Better Auth after-middleware catch-all** (leaveOrganization + team mutations) -4. **Atomic writes across betterAuth and mirror** (same transaction, or ordered with rollback guards) -5. **Hourly reconciliation cron** (defense-in-depth, repair drift) -6. **Read-time validation** (mirror + trust-layer override, not mirror-only) - -**ALTERNATIVE (SAFER)**: Keep betterAuth member table as authoritative source, use mirror as a **verified read cache** (check at write-time, never assume correctness at read-time). - ---- - -### SECTION I: ENUMERATION OF ALL DRIFT RISKS - -#### **RISK CLASS 1: PRIVILEGE RETENTION (User removed but still has access)** - -| # | Scenario | Severity | Root Cause | Impact | -| --- | ---------------------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 1.1 | User removed via `/organization/remove-member` endpoint (NOT via custom Convex mutation) | **CRITICAL** | Better Auth's after-hook does not exist; endpoint mutates member table directly, mirror is never touched | User deleted from betterAuth but visible in mirror; reads show organization membership | -| 1.2 | User leaves org via `/organization/leave` endpoint | **CRITICAL** | Same as 1.1; no hook for leaveOrganization | User self-removes from betterAuth but mirror row persists | -| 1.3 | Race: Custom addMember mutation writes to betterAuth, network fails before mirror write | **HIGH** | Non-atomic across components (betterAuth transaction commit ≠ Convex transaction commit) | If custom mutation fails part-way (adapter.create succeeds, ctx.db.insert fails), betterAuth has member but mirror doesn't; RLS allows access; next query re-queries betterAuth correctly, but data inconsistency window exists | -| 1.4 | Organization deleted via `authClient.organization.delete()` client call | **CRITICAL** | Better Auth plugin's cascade deletes member rows, but no hook to sync mirror; `delete_cleanup` only clears personalization | Org deleted from betterAuth, all member rows gone, mirror table still has stale rows for deleted org; if org is recreated with same ID (unlikely but possible in UUID collision scenarios), old mirror rows grant access to new org | -| 1.5 | Team member removed via `/organization/remove-team-member` endpoint | **HIGH** | No after-hook for team member mutations; mirror doesn't track team membership | User removed from team but mirrorMembership preserved; RLS doesn't scope to team, so privilege retention depends on downstream team-checking logic (getUserTeamIds call); if caller skips getTeamIds, team isolation broken | -| 1.6 | Org member demoted from admin → member via `/organization/update-member-role` endpoint | **HIGH** | No after-hook; custom mutations instrument sync, but direct endpoint calls bypass | User role changed to 'member' in betterAuth, mirror still shows 'admin' role; RLS reads mirror, grants admin perms; next read re-queries betterAuth (cache miss or fresh call), but evil admin can perform actions within the stale-read window | -| 1.7 | Backfill migration fails to populate mirror for existing members | **CRITICAL** | Backfill script crashes, logs error, continues; some orgs populated, others skipped; no idempotency check per-org | Mirror has gaps; users in skipped orgs report "not a member" error; admin re-runs backfill but add-member mutations race with it, creating duplicates or missing rows | -| 1.8 | Reconciliation cron times out on org with 10k members, wraps to next org | **HIGH** | Cursor persists mid-org; cron can only fix partial drift per run; if role-change rate > cron-rate, drift accumulates | Some users in large org never reconciled; their stale roles persist indefinitely until next cron cycle, then another timeout, then next... (could be weeks) | -| 1.9 | Trusted headers mode: JWT claim `trustedRole=admin` but betterAuth.member.role=member | **HIGH** | JWT override in getUserOrganizations is applied at read-time, but if mirror replaces getUserOrganizations, JWT claim is lost | If mirror read replaces adapter.findMany entirely, trusted-role override is gone; admin JWT claim ignored; user reads as member instead of admin | - -#### **RISK CLASS 2: WRONGFUL DENIAL / LOCKOUT (User should have access but doesn't)** - -| # | Scenario | Severity | Root Cause | Impact | -| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | -| 2.1 | User accepts invitation via `/organization/accept-invitation`; hook fires and persists audit, but mirror insert fails | **CRITICAL** | Mirror insert is separate from betterAuth write; audit logged, but mirror not updated | User joined in betterAuth (member row exists), audit shows join, but mirror doesn't; next RLS check queries mirror, gets no row, throws UnauthorizedError; user locked out despite being real member | -| 2.2 | Org creation: Better Auth creates org + owner member, mirror backfill hasn't run yet, user queries tasks | **CRITICAL** | Backfill happens async; org created at T0, backfill task scheduled but runs at T1, RLS read happens at T0.5 | User created new org, tries to list their own tasks, RLS checks getUserOrganizations → adapter.findMany gets the row, but if we've switched to mirror-only reads, mirror is empty → UnauthorizedError | -| 2.3 | SSO flow creates user + adds to org via `findOrCreateSsoUser`; network glitch during mirror insert | **HIGH** | SSO mutation atomicity: betterAuth write succeeds, mirror write fails, no retry | SSO user auto-provisioned in betterAuth but mirror missing; user can't access org; SSO flow doesn't expose error to client; user sees "unauthorized"; SSO provider confirms user exists in org, but app denies access | -| 2.4 | Admin removes user from org (removeMember mutation), cascadeOnMemberRemoved hard-deletes userMemories, but then mirror reconciliation deletes mirror row → admin is confused because they see 0 members, but audit log shows the user was deleted | **LOW** (Data consistency, not access) | Race between cascade deletion and mirror gc; no actual lockout, but audit trail confusing | Admin sees inconsistent state; not a security breach, but operational confusion | -| 2.5 | getUserTeamIds short-circuits on JWT `trustedTeams` claim, but mirror-based getUserOrganizations filters out the org the teams belong to (teams orphaned from org) | **HIGH** | Two independent read paths (one trusted-header short-circuit, one mirror-based) can diverge; team IDs from JWT don't match org membership in mirror | User has teams in JWT but org is missing from mirror → RLS allows team access but denies org access; RLS check for org context fails; user can't fetch org-scoped resources | -| 2.6 | Reconciliation cron queries betterAuth adapter for current state while a concurrent addMember mutation is in-flight | **MEDIUM** | Non-deterministic timing: cron reads, mutation writes, cron's snapshot is stale | Cron sees row count N, mutation adds row (N+1), cron deletes row (thinking it's stale), then mutation's mirror write fails because row was already deleted; eventual consistency, but brief lockout | -| 2.7 | `getOrganizationMember` email-fallback path returns mirror row instead of betterAuth row; email differs (user linked social account), fallback branching broken | **MEDIUM** | If mirror replaces both getUserOrganizations and getOrganizationMember, the email-fallback query logic (lines 50-77 in getOrganizationMember) is lost | Social-linked user's account migration path fails; email lookup doesn't happen; user gets UnauthorizedError instead of fallback | 2.8 | Team cascading delete via Entra ID SSO sync removes all teamMembers, but org-level mirror not updated; user still sees org membership but no teams | **MEDIUM** | Team deletion doesn't trigger org-level mirror update; inconsistency between org membership (mirror) and team membership (betterAuth.teamMember) | User sees org but teams are gone; downstream code expecting ≥1 team per org fails | - -#### **RISK CLASS 3: ROLE NORMALIZATION & VALIDATION DIVERGENCE** - -| # | Scenario | Severity | Root Cause | Impact | -| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | -| 3.1 | Role stored as "ADMIN" (uppercase) in betterAuth, mirror normalizes to "admin" (lowercase), but RLS check compares roles case-sensitively | **MEDIUM** | Mirror normalization (line 86 getUserOrganizations: `rawRole.toLowerCase()`) is implicit; if mirror reads skip normalization, roles diverge | RLS check: authorizeRls("ADMIN", "projects", "read") → fails because switch statement expects lowercase (auth.ts:276-282) | -| 3.2 | Custom mutation accepts role="developer", stores as "developer" in betterAuth, but isValidRole check (line 21 getUserOrganizations) fails because "developer" not in VALID_ROLES set | **HIGH** | VALID_ROLES hardcoded in getUserOrganizations; if a custom mutation or SSO flow adds "developer" but it's not in the set, role becomes "member" on read (fallback at line 88) | User granted "developer" role, but reads as "member"; they lose read-write access to projects/agents | -| 3.3 | Trusted headers JWT claim says `trustedRole="owner"`, but mirror stores `role="admin"` for same user (e.g., SSO metadata says owner, betterAuth plugin says admin) | **MEDIUM** | JWT and DB can diverge; getUserOrganizations applies JWT override (line 85: `const rawRole = trustedData?.trustedRole | | member.role`), but mirror read wouldn't have access to trustedData | Mirror read returns {role: "admin"}, JWT override logic is lost; user reads as admin instead of owner | -| 3.4 | Disabled role: getUserOrganizations filters out disabled rows (line 98-99 filter), but RLS logic treats disabled as "no perms" (disabled role in auth.ts:195-218 has empty permissions) | **LOW** | Two different handling: one filters, one allows row but denies perms; inconsistent semantics | Disabled user can be queried from betterAuth but not from RLS; defensive filter works, but semantics confusing | - -#### **RISK CLASS 4: ATOMIC & TRANSACTION FAILURE SCENARIOS** - -| # | Scenario | Severity | Root Cause | Impact | -| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| 4.1 | addMember mutation calls adapter.create (betterAuth txn), gets back memberId, then tries to log audit; audit log succeeds, but ctx.db insert into mirror fails (DB error / quota / timeout) | **CRITICAL** | Convex mutation is single transaction; if ctx.db.insert fails AFTER adapter.create succeeds, entire mutation is rolled back; BUT betterAuth adapter write is already committed (separate Convex component, committed immediately) | User added to betterAuth, mutation rolled back to client as error, user queries app, calls getUserOrganizations, adapter returns the new member row, user can access org, but mirror is empty; next RLS call queries adapter (still works), but mirror-based reads would deny access | -| 4.2 | removeMember mutation: adapter.deleteOne succeeds, cascadeOnMemberRemoved succeeds, but mirror delete fails | **CRITICAL** | Same transaction boundary issue; member deleted from betterAuth (committed), personalization cleaned up, mirror delete fails | User removed from betterAuth (audit trail confirms), but mirror still has row; user queries tasks, RLS reads mirror, sees membership, grants access; user still sees org data they were removed from | -| 4.3 | updateMemberRole: adapter.updateMany succeeds (role changed to "member"), but mirror patch fails | **HIGH** | Same; role changed in betterAuth (committed), mirror not updated | User role changed from admin → member in betterAuth (correct), but mirror still shows admin; RLS reads mirror, grants admin perms; user still has write access | -| 4.4 | Hook fires for afterAcceptInvitation; audit log written, but no hook to update mirror; mirror row doesn't exist yet | **CRITICAL** | Mirror backfill backlog; if backfill hasn't run yet, there's no pre-existing row to update; hook would need to INSERT into mirror, but that logic doesn't exist | User accepts invitation, joins betterAuth, audit logged, mirror not created; next RLS read queries mirror (assuming mirror-only reads), gets no row, user denied access; meanwhile betterAuth has the row | - -#### **RISK CLASS 5: PERMISSION MODEL & ACCESS CONTROL DIVERGENCE** - -| # | Scenario | Severity | Root Cause | Impact | -| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 5.1 | Mirror stores role="editor", RLS reads mirror, calls authorizeRls("editor", "projects", "write") | **MEDIUM** | Editor has no write permission (auth.ts:142-167); but if RLS code somehow invoked with cached "editor" role that was already normalized, permission check should work. Risk is if cache stale. | Should work if authorization layer is correct; risk is if cached role becomes stale due to mirror lag | -| 5.2 | Legal hold: org is under legal hold; removeMember mutation checks assertNotHeld (line 179, members/mutations.ts); user is NOT removed; but if mirror-based RLS doesn't check legal hold, mirror can still show membership | **MEDIUM** | Legal hold is mutation-time guard; RLS reads don't check it. Mirror would just return the row as-is. | User on legal hold; admin tries to remove them, mutation rejects them; but if downstream RLS reads from mirror without checking hold status, user still has access; eventual consistency issue but not a direct RLS bypass | - ---- - -### SECTION II: DETAILED FAILURE SCENARIOS BY WRITE PATH - -#### **Write Path 1: addMember (Custom Mutation) — lines 37-116, members/mutations.ts** - -**Current State**: ✅ Calls adapter.create at line 78, can add mirror write inline - -**Atomicity Risk**: - -- adapter.create commits immediately (betterAuth component) -- Mirror ctx.db.insert is separate transaction -- If mirror fails, adapter.create is not rolled back -- User added to betterAuth but not mirror - -**Mitigation**: - -```typescript -// AFTER adapter.create succeeds: -const memberId = String(isBetterAuthCreateResult(created) ? created._id : created); - -// NEW: Sync to mirror (MUST be before audit log, so failures are visible) -try { - await ctx.db.insert('memberMirror', { - organizationId: args.organizationId, - memberId, - userId: args.userId, - role: role ?? 'member', - createdAt: Date.now(), - }); -} catch (err) { - // CRITICAL: Mirror write failed. User is in betterAuth but not mirror. - // Don't swallow this error — log and throw so client knows to retry. - console.error('[addMember] mirror sync failed', { memberId, err }); - throw new Error(`Member added to auth but mirror sync failed: ${err}`); -} - -// Now safe to log audit -await AuditLogHelpers.logSuccess(...); -``` - -**Risk if skipped**: User added to org via custom mutation, mirror doesn't update, future RLS calls query mirror (assuming mirror-only reads), deny access. - ---- - -#### **Write Path 2: removeMember (Custom Mutation) — lines 118-220, members/mutations.ts** - -**Current State**: ✅ Calls adapter.deleteOne at line 189, can add mirror delete inline - -**Atomicity Risk**: - -- adapter.deleteOne commits immediately -- cascadeOnMemberRemoved deletes userMemories/userPreferences -- Mirror delete is separate transaction -- If mirror delete fails, user is removed from betterAuth but mirror still shows membership → PRIVILEGE RETENTION - -**Mitigation**: - -```typescript -// AFTER adapter.deleteOne succeeds: -await ctx.runMutation(components.betterAuth.adapter.deleteOne, { - input: { - model: 'member', - where: [{ field: '_id', value: args.memberId, operator: 'eq' }], - }, -}); - -// NEW: Sync to mirror (BEFORE cascade, so failures are visible) -const mirrorRow = await ctx.db - .query('memberMirror') - .withIndex('by_memberId', (q) => q.eq('memberId', args.memberId)) - .first(); -if (mirrorRow) { - try { - await ctx.db.delete(mirrorRow._id); - } catch (err) { - console.error('[removeMember] mirror delete failed', { - memberId: args.memberId, - err, - }); - throw new Error( - `Member deleted from auth but mirror delete failed: ${err}`, - ); - } -} - -// Now safe to cascade -if (member.userId) { - await cascadeOnMemberRemoved(ctx, member.userId, member.organizationId); -} -``` - -**Risk if skipped**: User removed from betterAuth, mirror not deleted, RLS reads mirror, grants access, user retains privilege. - ---- - -#### **Write Path 3: updateMemberRole (Custom Mutation) — lines 222-368, members/mutations.ts** - -**Current State**: ✅ Calls adapter.updateMany at line 338, can add mirror patch inline - -**Atomicity Risk**: - -- adapter.updateMany commits immediately -- Mirror patch is separate transaction -- If mirror patch fails, role changed in betterAuth but not mirror → ROLE ESCALATION (if demoted but mirror not updated) or ROLE DOWNGRADE (if promoted but mirror not updated) - -**Mitigation**: - -```typescript -await ctx.runMutation(components.betterAuth.adapter.updateMany, { - input: { - model: 'member', - where: [{ field: '_id', value: args.memberId, operator: 'eq' }], - update: { role: newRole }, - }, - paginationOpts: { cursor: null, numItems: 1 }, -}); - -// NEW: Sync to mirror -const mirrorRow = await ctx.db - .query('memberMirror') - .withIndex('by_memberId', (q) => q.eq('memberId', args.memberId)) - .first(); -if (mirrorRow) { - try { - await ctx.db.patch(mirrorRow._id, { - role: newRole, - updatedAt: Date.now(), - }); - } catch (err) { - console.error('[updateMemberRole] mirror patch failed', { - memberId: args.memberId, - err, - }); - throw new Error( - `Member role updated in auth but mirror patch failed: ${err}`, - ); - } -} -``` - -**Risk if skipped**: User demoted from admin to member in betterAuth, mirror not updated, RLS reads mirror, user still reads as admin, performs unauthorized actions. - ---- - -#### **Write Path 4: transferOwnership (Custom Mutation) — lines 370-467, members/mutations.ts** - -**Current State**: ✅ Calls adapter.updateMany at lines 415 and 425 (two separate updates) - -**Atomicity Risk**: - -- Two separate adapter.updateMany calls (promote target, demote caller) -- Each is a separate betterAuth transaction -- Mirror patches are separate Convex transactions -- If first adapter.updateMany succeeds, second adapter.updateMany fails, OR any mirror patch fails → inconsistent state (target not promoted, caller not demoted) - -**Mitigation**: - -```typescript -// Promote target to owner -await ctx.runMutation(components.betterAuth.adapter.updateMany, { - input: { - model: 'member', - where: [{ field: '_id', value: args.targetMemberId, operator: 'eq' }], - update: { role: 'owner' }, - }, - paginationOpts: { cursor: null, numItems: 1 }, -}); - -// NEW: Mirror patch for target promotion -const targetMirrorRow = await ctx.db - .query('memberMirror') - .withIndex('by_memberId', (q) => q.eq('memberId', args.targetMemberId)) - .first(); -if (targetMirrorRow) { - try { - await ctx.db.patch(targetMirrorRow._id, { - role: 'owner', - updatedAt: Date.now(), - }); - } catch (err) { - console.error('[transferOwnership] target mirror patch failed', err); - throw new Error(`Target promoted in auth but mirror patch failed: ${err}`); - } -} - -// Demote caller from owner to admin -await ctx.runMutation(components.betterAuth.adapter.updateMany, { - input: { - model: 'member', - where: [{ field: '_id', value: callerMemberId, operator: 'eq' }], - update: { role: 'admin' }, - }, - paginationOpts: { cursor: null, numItems: 1 }, -}); - -// NEW: Mirror patch for caller demotion -const callerMirrorRow = await ctx.db - .query('memberMirror') - .withIndex('by_memberId', (q) => q.eq('memberId', callerMemberId)) - .first(); -if (callerMirrorRow) { - try { - await ctx.db.patch(callerMirrorRow._id, { - role: 'admin', - updatedAt: Date.now(), - }); - } catch (err) { - console.error('[transferOwnership] caller mirror patch failed', err); - throw new Error(`Caller demoted in auth but mirror patch failed: ${err}`); - } -} -``` - -**Risk if skipped**: Target not promoted, caller not demoted, org has no owner or two owners, RLS becomes inconsistent. - ---- - -#### **Write Path 5: afterCreateOrganization (Better Auth Hook) — lines 724-766, auth.ts** - -**Current State**: ❌ No mirror insert in this hook - -**Why it's called**: When `authClient.organization.create()` is called, Better Auth plugin creates org + inserts member row with role='owner' for creator. Hook fires AFTER member row is persisted. - -**Atomicity Risk**: - -- Better Auth has already committed the org + member row -- Hook is called to do post-processing (audit, scaffolding) -- Mirror write is not in the hook -- If mirror write is missing, creator joined betterAuth but not mirror - -**Mitigation** (in auth.ts afterCreateOrganization hook): - -```typescript -afterCreateOrganization: async (data) => { - // NEW: Sync member row to mirror - try { - const runCtx = requireRunMutationCtx(ctx); - await runCtx.runMutation( - internal.members.mirror_sync.syncMemberToMirror, - { - memberId: data.member._id, - organizationId: data.organization.id, - userId: data.user.id, - role: data.member.role, - }, - ); - } catch (err) { - console.error( - '[afterCreateOrganization] failed to sync member to mirror', - err instanceof Error ? err.message : err, - ); - // Don't throw; org is already created. Log and continue. - // Reconciliation cron will fix the missing mirror row. - } - - // ... existing scaffolding and audit logging ... -}, -``` - -**Risk if skipped**: Org created, member row inserted in betterAuth, mirror empty, next RLS call queries mirror (if mirror-only reads), creator denied access to their own org. - ---- - -#### **Write Path 6: afterAcceptInvitation (Better Auth Hook) — lines 767-790, auth.ts** - -**Current State**: ❌ No mirror insert in this hook - -**Why it's called**: When `authClient.organization.acceptInvitation(invitationId)` is called, Better Auth plugin creates member row with the role from invitation. Hook fires AFTER member row is persisted. - -**Atomicity Risk**: Same as afterCreateOrganization. - -**Mitigation** (same pattern): - -```typescript -afterAcceptInvitation: async (data) => { - // NEW: Sync member row to mirror - try { - const runCtx = requireRunMutationCtx(ctx); - await runCtx.runMutation( - internal.members.mirror_sync.syncMemberToMirror, - { - memberId: data.member._id, - organizationId: data.organization.id, - userId: data.user.id, - role: data.member.role, - }, - ); - } catch (err) { - console.error( - '[afterAcceptInvitation] failed to sync member to mirror', - err instanceof Error ? err.message : err, - ); - // Don't throw; user is already added. Log and continue. - // Reconciliation cron will fix the missing mirror row. - } - - // ... existing audit logging ... -}, -``` - -**Risk if skipped**: User accepts invitation, member row created in betterAuth, mirror empty, next RLS call queries mirror, user denied access to org they just joined. - ---- - -#### **Write Path 7: leaveOrganization (/organization/leave endpoint) — NO CUSTOM HANDLER** - -**Current State**: ❌ No custom mutation; client calls `authClient.organization.leaveOrganization()` directly - -**Why it's a problem**: Better Auth endpoint deletes the caller's member row from betterAuth, but there is NO hook in the organization plugin config to catch member deletions. The after-middleware in auth.ts (lines 463-553) does NOT monitor `/organization/leave`. - -**Atomicity Risk**: - -- Better Auth endpoint deletes member row -- No hook fires -- No mirror delete triggered -- User left betterAuth but mirror still shows membership - -**Mitigation Option A: Add catch-all to after-middleware** (in auth.ts): - -```typescript -after: createAuthMiddleware(async (mw) => { - // ... existing code ... - - // NEW: Catch member/team mutations that have no hooks - if (mw.path === '/organization/leave') { - const body = isRecord(mw.body) ? mw.body : {}; - const organizationId = getString(body, 'organizationId'); - - if (organizationId && mw.context.newSession) { - // User just left org. Their member row is already deleted from betterAuth. - // But mirror still has it. Schedule a background action to sync. - try { - const runCtx = requireRunMutationCtx(ctx); - const userId = mw.context.newSession?.userId || mw.context.session?.userId; - if (userId) { - await runCtx.scheduler.runAfter( - 0, - internal.members.mirror_sync.syncMemberRemovalFromOrg, - { organizationId, userId }, - ); - } - } catch (err) { - console.warn( - '[after-middleware /organization/leave] failed to schedule mirror sync', - err instanceof Error ? err.message : err, - ); - // Non-fatal; reconciliation cron will clean up. - } - } - } - - if (mw.path === '/organization/remove-member') { - // Existing custom mutation handles this, but if called directly via endpoint... - // (Better Auth's org plugin has this endpoint; we disable it via access control, - // but defensive check here) - const body = isRecord(mw.body) ? mw.body : {}; - const organizationId = getString(body, 'organizationId'); - const memberId = getString(body, 'memberId') || getString(body, 'memberIdOrEmail'); - - if (organizationId && memberId && mw.context.returned && !mw.context.returned instanceof APIError) { - // Endpoint succeeded; member deleted. Trigger mirror sync. - try { - const runCtx = requireRunMutationCtx(ctx); - await runCtx.scheduler.runAfter( - 0, - internal.members.mirror_sync.syncMemberDeleted, - { memberId, organizationId }, - ); - } catch (err) { - console.warn( - '[after-middleware /organization/remove-member] failed to schedule mirror sync', - err instanceof Error ? err.message : err, - ); - } - } - } - - // Similar patterns for: - // /organization/update-member-role (role changes) - // /organization/add-team-member (team additions) - // /organization/remove-team-member (team removals) - // /organization/delete (org deletion cascades) - // ... (full implementation in next section) -}), -``` - -**Risk if skipped**: User leaves org, member row deleted from betterAuth, mirror not updated, user still appears as member in RLS reads, retains access to org data. - ---- - -#### **Write Path 8-12: SSO Flows (findOrCreateSsoUser, findOrCreateUserFromHeaders, addMemberInternal, createMember, createUserWithoutSession)** - -**Current State**: ✅ Each calls adapter.create, can add mirror writes inline - -**Atomicity Risk**: Same as addMember (adapter.create, then ctx.db.insert). - -**Mitigation** (template, applies to all 5 paths): - -```typescript -// AFTER adapter.create succeeds: -const memberId = String( - isBetterAuthCreateResult(created) ? created._id : created, -); - -// NEW: Sync to mirror -try { - await ctx.db.insert('memberMirror', { - organizationId: args.organizationId, - memberId, - userId: args.userId, - role: role ?? 'member', - createdAt: Date.now(), - }); -} catch (err) { - console.error('[SSO flow] mirror sync failed', { memberId, err }); - throw new Error(`Member created but mirror sync failed: ${err}`); -} -``` - -**Risk if skipped**: SSO user auto-provisioned in betterAuth but not mirror, user denied access on first org access after SSO login. - ---- - -#### **Write Path 13-14: Team Member Mutations (addMember, removeMember in team_members/mutations.ts)** - -**Current State**: ✅ Calls adapter.create/deleteOne, but **team membership is separate from org membership** - -**Key Design Decision**: Team membership does NOT need a separate mirror because: - -- `getUserTeamIds` queries betterAuth.teamMember directly (lines 86-104, get_user_teams.ts) -- Team isolation is enforced at query time via `getUserTeamIds` result, not via RLS role check -- Team membership is a **soft permission** (affects dataset scope), not a hard role -- RLS is computed from org membership (admin/member/editor/etc.), not team membership - -**BUT team mutations DO affect org membership context**: - -- When last team member is removed, team is deleted (cascade, line 233 entra_id/team_sync.ts) -- Team belongs to org; team deletion doesn't cascade to org membership -- BUT if team deletion removes org context (unlikely), RLS could be affected - -**Mitigation**: - -- No separate teamMember mirror needed -- Ensure team-deletion cascades are correct in Entra ID sync (test coverage) -- If future design uses team membership for RLS, create teamMemberMirror following same pattern - ---- - -#### **Write Path 15: Organization Deletion (authClient.organization.delete())** - -**Current State**: ❌ No hook, no after-middleware catch - -**Why it's a problem**: Better Auth plugin cascades delete on all member rows for the org, but mirror rows persist. If org is recreated with same ID (UUID collision, extremely rare, but possible), old mirror rows grant access to new org. - -**Atomicity Risk**: - -- Better Auth deletes org + all member rows -- Mirror rows persist (orphaned) -- delete_cleanup is called BEFORE org deletion (lines 79-80 comment in organizations/delete_cleanup.ts) -- delete_cleanup only cascades personalization, not mirror - -**Mitigation A: Add to delete_cleanup** (organizations/delete_cleanup.ts, around line 82): - -```typescript -// NEW: Clean up mirror rows for all members in this org -try { - const organizationId = (orgRecord as { id?: string })?.id; - if (organizationId) { - const mirrorRows = await ctx.db - .query('memberMirror') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', organizationId), - ) - .collect(); - for (const row of mirrorRows) { - await ctx.db.delete(row._id); - } - } -} catch (err) { - console.error('[deleteOrganization] failed to clean mirror', err); - throw err; // Fail hard so org isn't deleted with orphaned mirror rows -} -``` - -**Mitigation B: Add to after-middleware** (auth.ts): - -```typescript -if (mw.path === '/organization/delete') { - const body = isRecord(mw.body) ? mw.body : {}; - const organizationId = getString(body, 'organizationId'); - - if (organizationId && (!mw.context.returned) instanceof APIError) { - // Org deleted. Cascade delete mirror rows. - try { - const runCtx = requireRunMutationCtx(ctx); - await runCtx.scheduler.runAfter( - 0, - internal.members.mirror_sync.cascadeDeleteOrgMembers, - { organizationId }, - ); - } catch (err) { - console.warn( - '[after-middleware /organization/delete] failed to cascade mirror', - err, - ); - } - } -} -``` - -**Risk if skipped**: Org deleted, mirror rows orphaned, if same UUID is reused, old mirror rows grant access to new org (extremely low probability, but possible). - ---- - -#### **Write Path 16: Migration (migrate_org_creators)** - -**Current State**: ✅ Calls adapter.updateMany to promote org creators to owner role - -**Issue**: When migration runs, if mirror already exists, mirror rows are NOT updated. But if migration runs BEFORE mirror backfill, then backfill will have the correct role. If migration runs AFTER backfill, mirror rows won't be updated. - -**Mitigation** (in migrate_org_creators.ts): - -```typescript -// AFTER adapter.updateMany succeeds: -await ctx.runMutation(components.betterAuth.adapter.updateMany, { - input: { - model: 'member', - where: [{ field: '_id', value: creator._id, operator: 'eq' }], - update: { role: 'owner' }, - }, - paginationOpts: { cursor: null, numItems: 1 }, -}); - -// NEW: Mirror sync (only if mirror exists; backfill is source of truth initially) -const mirrorRow = await ctx.db - .query('memberMirror') - .withIndex('by_memberId', (q) => q.eq('memberId', creator._id)) - .first(); -if (mirrorRow) { - try { - await ctx.db.patch(mirrorRow._id, { - role: 'owner', - updatedAt: Date.now(), - }); - } catch (err) { - console.error('[migrate_org_creators] mirror patch failed', err); - // Don't throw; migration is idempotent, can re-run - } -} -``` - -**Risk if skipped**: Migration promotes creators to owner in betterAuth, but mirror still shows member/admin role, RLS reads mirror, creator reads as lower role. - ---- - -### SECTION III: AFTER-MIDDLEWARE CATCH-ALL IMPLEMENTATION - -The after-middleware in auth.ts (lines 463-553) must be extended to catch member/team mutations that have no hooks: - -```typescript -after: createAuthMiddleware(async (mw) => { - // ... existing code for 2FA, API key suffix, etc. ... - - // NEW SECTION: Sync member/team mutations to mirror - const path = mw.path; - const body = isRecord(mw.body) ? mw.body : {}; - const returned = mw.context.returned; - const isError = returned instanceof APIError; - const runCtx = requireRunMutationCtx(ctx); - - // Only process successful responses - if (!isError) { - // /organization/leave: User self-removed from org - if (path === '/organization/leave') { - const organizationId = getString(body, 'organizationId'); - const userId = mw.context.session?.userId; - if (organizationId && userId) { - try { - await runCtx.scheduler.runAfter( - 0, - internal.members.mirror_sync.syncMemberRemovalFromOrg, - { organizationId, userId }, - ); - } catch (err) { - console.warn('[leave-org] failed to schedule mirror sync', err); - } - } - } - - // /organization/remove-member: Admin removed member (direct endpoint call, not custom mutation) - if (path === '/organization/remove-member') { - const organizationId = getString(body, 'organizationId'); - const memberId = getString(body, 'memberIdOrEmail'); // Better Auth uses this field name - if (organizationId && memberId) { - try { - await runCtx.scheduler.runAfter( - 0, - internal.members.mirror_sync.syncMemberDeleted, - { memberId, organizationId }, - ); - } catch (err) { - console.warn('[remove-member] failed to schedule mirror sync', err); - } - } - } - - // /organization/update-member-role: Role change (direct endpoint call, not custom mutation) - if (path === '/organization/update-member-role') { - const memberId = getString(body, 'memberId'); - const organizationId = getString(body, 'organizationId'); - const newRole = getString(body, 'role'); - if (memberId && organizationId && newRole) { - try { - await runCtx.scheduler.runAfter( - 0, - internal.members.mirror_sync.syncMemberRoleChange, - { memberId, organizationId, newRole }, - ); - } catch (err) { - console.warn('[update-role] failed to schedule mirror sync', err); - } - } - } - - // /organization/add-team-member: Team member addition - if (path === '/organization/add-team-member') { - const organizationId = getString(body, 'organizationId'); - const userId = getString(body, 'userId'); - const teamId = getString(body, 'teamId'); - if (organizationId && userId && teamId) { - // Team membership doesn't require mirror sync (team-related isolation - // is handled by getUserTeamIds query, not RLS role). But log it for audit. - console.info('[add-team-member] user added to team', { - organizationId, - userId, - teamId, - }); - } - } - - // /organization/remove-team-member: Team member removal - if (path === '/organization/remove-team-member') { - const organizationId = getString(body, 'organizationId'); - const userId = getString(body, 'userId'); - const teamId = getString(body, 'teamId'); - if (organizationId && userId && teamId) { - // Team membership doesn't require mirror sync. Log for audit. - console.info('[remove-team-member] user removed from team', { - organizationId, - userId, - teamId, - }); - } - } - - // /organization/delete: Organization deleted - if (path === '/organization/delete') { - const organizationId = getString(body, 'organizationId'); - if (organizationId) { - try { - await runCtx.scheduler.runAfter( - 0, - internal.members.mirror_sync.cascadeDeleteOrgMembers, - { organizationId }, - ); - } catch (err) { - console.warn('[delete-org] failed to schedule mirror cascade', err); - } - } - } - } -}), -``` - ---- - -### SECTION IV: MIRROR SYNC HELPER MUTATIONS - -Create `/convex/members/mirror_sync.ts`: - -```typescript -import { v } from 'convex/values'; -import { components } from '../_generated/api'; -import { internalMutation } from '../_generated/server'; - -/** - * Sync a single member row to mirror (insert). - * Called after betterAuth member is created. - */ -export const syncMemberToMirror = internalMutation({ - args: { - memberId: v.string(), - organizationId: v.string(), - userId: v.string(), - role: v.string(), - }, - handler: async (ctx, args) => { - const existing = await ctx.db - .query('memberMirror') - .withIndex('by_memberId', (q) => q.eq('memberId', args.memberId)) - .first(); - - if (existing) { - // Idempotent: already synced - return; - } - - await ctx.db.insert('memberMirror', { - organizationId: args.organizationId, - memberId: args.memberId, - userId: args.userId, - role: (args.role ?? 'member').toLowerCase(), - createdAt: Date.now(), - updatedAt: Date.now(), - }); - }, -}); - -/** - * Delete all mirror rows for a user in an org. - * Called when user leaves or is removed from org. - */ -export const syncMemberRemovalFromOrg = internalMutation({ - args: { - organizationId: v.string(), - userId: v.string(), - }, - handler: async (ctx, args) => { - const mirrorRows = await ctx.db - .query('memberMirror') - .withIndex('by_org_user', (q) => - q.eq('organizationId', args.organizationId).eq('userId', args.userId), - ) - .collect(); - - for (const row of mirrorRows) { - await ctx.db.delete(row._id); - } - }, -}); - -/** - * Delete a specific member mirror row. - * Called when a member is deleted from org. - */ -export const syncMemberDeleted = internalMutation({ - args: { - memberId: v.string(), - organizationId: v.string(), - }, - handler: async (ctx, args) => { - const mirrorRow = await ctx.db - .query('memberMirror') - .withIndex('by_memberId', (q) => q.eq('memberId', args.memberId)) - .first(); - - if (mirrorRow) { - await ctx.db.delete(mirrorRow._id); - } - }, -}); - -/** - * Update a member's role in mirror. - * Called when member's role changes. - */ -export const syncMemberRoleChange = internalMutation({ - args: { - memberId: v.string(), - organizationId: v.string(), - newRole: v.string(), - }, - handler: async (ctx, args) => { - const mirrorRow = await ctx.db - .query('memberMirror') - .withIndex('by_memberId', (q) => q.eq('memberId', args.memberId)) - .first(); - - if (mirrorRow) { - await ctx.db.patch(mirrorRow._id, { - role: (args.newRole ?? 'member').toLowerCase(), - updatedAt: Date.now(), - }); - } - }, -}); - -/** - * Cascade delete all mirror rows for an org. - * Called when org is deleted. - */ -export const cascadeDeleteOrgMembers = internalMutation({ - args: { - organizationId: v.string(), - }, - handler: async (ctx, args) => { - const mirrorRows = await ctx.db - .query('memberMirror') - .withIndex('by_organizationId', (q) => - q.eq('organizationId', args.organizationId), - ) - .collect(); - - for (const row of mirrorRows) { - await ctx.db.delete(row._id); - } - }, -}); -``` - ---- - -### SECTION V: RECONCILIATION CRON (Defense-in-Depth) - -Create `/convex/members/mirror_reconciliation.ts` (from earlier in this analysis): - -**Runs hourly**, scans up to 20 orgs per run, compares mirror against betterAuth, repairs drift: - -- Deleted in betterAuth but mirror exists → delete mirror -- Role changed in betterAuth but mirror not updated → patch mirror -- Cursor persists to resume next run - -**Prevents accumulation of drift** from partial failures, timing windows, missed mutations. - ---- - -### SECTION VI: BACKFILL MIGRATION - -Create `/convex/migrations/backfill_member_mirror.ts` (initial population): - -**Runs once**, reads all org members from betterAuth, populates mirror table. -**Idempotent**: skips rows that already exist. - ---- - -### SECTION VII: MIRROR READ SAFETY & AUTHORITATIVE SOURCE - -**CRITICAL DESIGN DECISION**: - -The mirror should **NOT be the sole source of truth** for RLS. Instead, it should be: - -1. **For cached reads (getUserOrganizations in request-scoped cache)**: - - Read mirror first (fast, local DB) - - If miss or stale, fallback to betterAuth - - Cache in request context (request_auth_cache.ts) so multiple RLS checks reuse - -2. **For authoritative reads (getOrganizationMember at RLS decision point)**: - - Query betterAuth directly (no mirror short-circuit) - - Apply JWT override (trustedRole) from trusted headers - - This is the critical gate; must be correct - -3. **For async operations (background actions, batch operations)**: - - Read mirror - - Reconciliation cron ensures eventual consistency - -**WHY NOT mirror-only reads?** - -1. **Trusted headers override**: Mirror doesn't store trustedRole JWT claim; mirror-only reads lose the override mechanism -2. **Email fallback**: getOrganizationMember has complex email-based recovery (lines 50-77) for account linking; mirror can't replicate this -3. **Atomicity windows**: Even with all mitigations, non-atomic writes create brief inconsistencies; betterAuth is the source of truth -4. **Cascades**: Email-based user lookups and social-linking flows depend on the user table; mirror can't replicate -5. **Team isolation**: getUserTeamIds already queries betterAuth (because mirror doesn't denormalize teams); consistency requires both paths use same source - -**Recommendation**: Use mirror as a **performance optimization** (cache/local join), not a **security boundary** (RLS gate). Keep betterAuth as the authoritative RLS source. - ---- - -### SECTION VIII: MINIMUM GUARANTEES FOR 100% SECURITY - -The implementation becomes "100% secure" only if: - -#### **Guarantee 1: No Privilege Retention** - -- ✅ Inline mirror delete on every adapter.deleteOne (removeMember, leaveOrganization via after-middleware) -- ✅ Cascade delete mirror rows on org deletion (via after-middleware or delete_cleanup) -- ✅ Reconciliation cron deletes orphaned mirror rows -- ✅ RLS reads betterAuth, not mirror (mirror is cache only) - -#### **Guarantee 2: No Wrongful Denial** - -- ✅ Inline mirror insert on every adapter.create (addMember, acceptInvitation via hook, SSO flows) -- ✅ Hooks in afterCreateOrganization and afterAcceptInvitation sync member row to mirror -- ✅ Backfill migration populates mirror for existing members -- ✅ Reconciliation cron inserts missing mirror rows -- ✅ RLS reads betterAuth on critical path, cache miss is recoverable - -#### **Guarantee 3: No Role Escalation/Downgrade** - -- ✅ Inline mirror patch on every adapter.updateMany (updateMemberRole, transferOwnership, migrations) -- ✅ After-middleware catches role changes from direct endpoint calls -- ✅ Reconciliation cron detects role divergence and repairs -- ✅ RLS reads betterAuth, not mirror, so role changes take effect immediately - -#### **Guarantee 4: Atomic Transactions** - -- ✅ Errors in mirror writes are caught and either thrown (blocking) or logged (non-blocking + reconciliation cron) -- ✅ Blocking errors (must-succeed paths like member deletion) bubble to client -- ✅ Non-blocking errors (audit logging) log warning and let cron fix it -- ✅ betterAuth is committed before mirror write, so if mirror fails, cron reconciles - -#### **Guarantee 5: Trust Layer Preserved** - -- ✅ JWT claims (trustedRole, trustedTeams) are separate from mirror -- ✅ Read-time logic applies JWT override (line 85 getUserOrganizations) -- ✅ Mirror is never the authoritative source for RLS gates - -#### **Guarantee 6: Email Fallback Preserved** - -- ✅ getOrganizationMember still queries betterAuth, not mirror -- ✅ Email-based account linking fallback (lines 50-77) still works -- ✅ Mirror is not used for critical RLS gates - ---- - -### SECTION IX: VERDICT — IS MIRROR-ONLY RLS SAFE? - -**NO. Reading the mirror authoritatively (as the sole source of truth) for RLS is UNSAFE, even with all mitigations, because:** - -1. **Trusted headers override is lost** — Mirror doesn't store the JWT claim -2. **Email fallback is lost** — Mirror can't replicate the user-table lookup -3. **Atomicity windows remain** — Non-atomic writes between betterAuth and mirror create brief inconsistencies -4. **Cache busting is complex** — Invalidating the mirror-as-authoritative-source requires monitoring all 21 write paths -5. **Partial failures are hard** — If mirror write fails after betterAuth write, the system is inconsistent until cron runs - -**SAFER ARCHITECTURE**: - -- Mirror = performance optimization (cache, reduce betterAuth queries) -- betterAuth = authoritative RLS source -- Trust layer = override mechanism (JWT claims) -- Reconciliation cron = eventual consistency safety net -- After-middleware = catch-all for mutations with no hooks - ---- - -### SECTION X: IMPLEMENTATION CHECKLIST - -#### **Phase 1: Mirror Infrastructure** - -- [ ] Create `memberMirror` table in schema.ts (fields: organizationId, memberId, userId, role, createdAt, updatedAt) -- [ ] Create indexes: by_memberId, by_organizationId, by_org_user -- [ ] Create `memberMirrorGcCursor` table for reconciliation cron cursor -- [ ] Create `/convex/members/mirror_sync.ts` with 5 helper mutations (syncMemberToMirror, syncMemberRemovalFromOrg, syncMemberDeleted, syncMemberRoleChange, cascadeDeleteOrgMembers) - -#### **Phase 2: Inline Sync in Custom Mutations** - -- [ ] members/mutations.ts: addMember — add mirror insert after adapter.create -- [ ] members/mutations.ts: removeMember — add mirror delete after adapter.deleteOne -- [ ] members/mutations.ts: updateMemberRole — add mirror patch after adapter.updateMany -- [ ] members/mutations.ts: transferOwnership — add mirror patches for both promote + demote -- [ ] sso_providers/find_or_create_sso_user.ts: add mirror insert (2 paths) -- [ ] betterAuth/trusted_headers/find_or_create_user_from_headers.ts: add mirror insert (2 paths) -- [ ] users/add_member_internal.ts: add mirror insert -- [ ] users/create_member.ts: add mirror insert (2 paths) -- [ ] users/create_user_without_session.ts: add mirror insert -- [ ] migrations/migrate_org_creators.ts: add mirror patch for owner promotions - -#### **Phase 3: Hooks in Better Auth Plugin** - -- [ ] auth.ts: afterCreateOrganization — add mirror insert via internal.members.mirror_sync.syncMemberToMirror -- [ ] auth.ts: afterAcceptInvitation — add mirror insert via internal.members.mirror_sync.syncMemberToMirror -- [ ] auth.ts: organizations/delete_cleanup.ts — add mirror cascade delete before org deletion - -#### **Phase 4: After-Middleware Catch-All** - -- [ ] auth.ts: after: createAuthMiddleware — add handlers for: - - [ ] /organization/leave → syncMemberRemovalFromOrg - - [ ] /organization/remove-member → syncMemberDeleted - - [ ] /organization/update-member-role → syncMemberRoleChange - - [ ] /organization/delete → cascadeDeleteOrgMembers - -#### **Phase 5: Reconciliation Cron** - -- [ ] Create /convex/members/mirror_reconciliation.ts with hourly cron -- [ ] Registers in crons.ts with schedule '0 \* \* \* \*' - -#### **Phase 6: Backfill Migration** - -- [ ] Create /convex/migrations/backfill_member_mirror.ts -- [ ] Register in migrations.ts:runAll -- [ ] Test: runAll, verify mirror populated - -#### **Phase 7: Tests** - -- [ ] Unit: sync helpers (insert, delete, patch, cascade) -- [ ] Connector: custom mutations create mirror rows -- [ ] Connector: hooks create mirror rows -- [ ] Connector: after-middleware schedules mirror sync -- [ ] Connector: reconciliation cron repairs drift -- [ ] End-to-end: addMember → mirror synced → RLS reads mirror cache (hits) → getOrganizationMember reads betterAuth (authoritative) → both agree - -#### **Phase 8: RLS Read Pattern (NO CHANGES)** - -- [ ] getOrganizationMember: Keep reading betterAuth (no mirror short-circuit) -- [ ] getUserOrganizations: Keep reading betterAuth with pagination (request cache layer can cache result) -- [ ] getUserTeamIds: Keep reading betterAuth.teamMember (no mirror for teams) -- [ ] JWT override (trustedRole) applied at read time (mirror-agnostic) - -#### **Phase 9: Monitoring & Alerts** - -- [ ] Alert: mirror.count < betterAuth.member.count by >5% for any org (indicates backlog) -- [ ] Alert: reconciliation cron rowsDeleted > 100/run (indicates drift rate) -- [ ] Log: after-middleware sync failures (non-fatal, but track frequency) -- [ ] Log: inline sync failures in mutations (fatal, should be rare) - ---- - -### FINAL RECOMMENDATION - -**DO NOT USE THE MIRROR AS THE AUTHORITATIVE RLS SOURCE.** - -Instead: - -1. **Create mirror** as a **performance cache** (reduce cross-component betterAuth queries in request-scoped context) -2. **Keep betterAuth as authoritative** for RLS gates (getOrganizationMember, final authz decision) -3. **Sync mirror inline** on all write paths (16 custom + 4 hooks + after-middleware catch-all) -4. **Reconciliation cron** as defense-in-depth (eventual consistency, error recovery) -5. **Trust layer (JWT)** separate from mirror (overrides at read time) -6. **Email fallback** still queries betterAuth (not mirrored) - -This architecture achieves: - -- ✅ **100% Security**: RLS never reads stale data; betterAuth is authoritative -- ✅ **Maximally Fast**: Request-scoped cache reuses org/team lookups within single function -- ✅ **Eventual Consistency**: Reconciliation cron repairs any drift from partial failures -- ✅ **Trustworthy**: Clear data flow (source → mirror → cache → RLS decision) - ---- - -**END OF ADVERSARIAL SECURITY AUDIT** diff --git a/services/platform/convex.json b/services/platform/convex.json deleted file mode 100644 index b277b6975f..0000000000 --- a/services/platform/convex.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "node_modules/convex/schemas/convex.schema.json", - "bundler": { - "includeSourcesContent": false - }, - "node": { - "externalPackages": [ - "@modelcontextprotocol/sdk", - "canvas", - "imapflow", - "mailparser", - "mssql", - "mysql2", - "nodemailer", - "pg", - "postgres", - "tedious", - "undici" - ] - }, - "aiFiles": { - "enabled": false - } -} diff --git a/services/platform/lib/auth-client.ts b/services/platform/lib/auth-client.ts index 17c4f00056..0aac833e73 100644 --- a/services/platform/lib/auth-client.ts +++ b/services/platform/lib/auth-client.ts @@ -1,6 +1,5 @@ import { apiKeyClient } from '@better-auth/api-key/client'; import { passkeyClient } from '@better-auth/passkey/client'; -import { convexClient } from '@convex-dev/better-auth/client/plugins'; import { organizationClient, twoFactorClient, @@ -62,7 +61,6 @@ export const authClient = createAuthClient({ }, }, plugins: [ - convexClient(), apiKeyClient(), // WebAuthn / passkeys (#1508). Exposes authClient.passkey.* for the // registration + authentication ceremonies the browser drives. diff --git a/services/platform/lib/shared/handlers/function-refs.test.ts b/services/platform/lib/shared/handlers/function-refs.test.ts index bf255221ab..695e4fc5a4 100644 --- a/services/platform/lib/shared/handlers/function-refs.test.ts +++ b/services/platform/lib/shared/handlers/function-refs.test.ts @@ -1,4 +1,3 @@ -import { getFunctionName, anyApi, componentsGeneric } from 'convex/server'; import { describe, expect, it } from 'vitest'; import { @@ -9,29 +8,41 @@ import { /** * The ctx shim's handler tables are keyed by the strings the RETIRED runtime's - * namer produced. This suite is the proof that ours produces the same ones — - * run against the package itself, while it is still installed, over every name - * the reused tree actually uses plus the shapes that are easy to get wrong - * (a `default` export, a nested module path, a component reference). - * - * When `convex` finally leaves `package.json`, the comparison goes and the - * table of expected strings stays: the format is ours to keep either way. + * namer produced. While the `convex` package was still installed, this suite + * compared our namer against the package's over every shape below; the package + * is gone, the comparison went with it, and the expected strings stay — the + * format is ours to keep. */ -// oxlint-disable-next-line typescript/no-explicit-any -- both sides are proxies; the test is about the strings they yield +// oxlint-disable-next-line typescript/no-explicit-any -- a recording proxy; the test is about the strings it yields type Refs = any; const ours: Refs = createFunctionRefs(); -const theirs: Refs = anyApi; -const PATHS: readonly string[][] = [ - ['tasks', 'helpers', 'recordActivity'], - ['audit_logs', 'internal_mutations', 'createAuditLog'], - ['node_only', 'sandbox', 'session_exec', 'runExec'], - ['chat', 'messages', 'appendMessageInternal'], - ['lib', 'config_store', 'actions', 'readConfigArea'], - ['enterprise_sso', 'login', 'callback_handler', 'default'], - ['provisioning', 'default'], +const CASES: readonly (readonly [readonly string[], string])[] = [ + [['tasks', 'helpers', 'recordActivity'], 'tasks/helpers:recordActivity'], + [ + ['audit_logs', 'internal_mutations', 'createAuditLog'], + 'audit_logs/internal_mutations:createAuditLog', + ], + [ + ['node_only', 'sandbox', 'session_exec', 'runExec'], + 'node_only/sandbox/session_exec:runExec', + ], + [ + ['chat', 'messages', 'appendMessageInternal'], + 'chat/messages:appendMessageInternal', + ], + [ + ['lib', 'config_store', 'actions', 'readConfigArea'], + 'lib/config_store/actions:readConfigArea', + ], + // A `default` export keeps only the module path. + [ + ['enterprise_sso', 'login', 'callback_handler', 'default'], + 'enterprise_sso/login/callback_handler', + ], + [['provisioning', 'default'], 'provisioning'], ]; function walk(root: Refs, path: readonly string[]): Refs { @@ -39,12 +50,10 @@ function walk(root: Refs, path: readonly string[]): Refs { } describe('functionRefName matches the retired runtime', () => { - it.each(PATHS.map((p) => [p.join('.'), p] as const))( + it.each(CASES.map(([p, expected]) => [p.join('.'), p, expected] as const))( 'names %s the same way', - (_label, path) => { - expect(functionRefName(walk(ours, path))).toBe( - getFunctionName(walk(theirs, path)), - ); + (_label, path, expected) => { + expect(functionRefName(walk(ours, path))).toBe(expected); }, ); @@ -76,15 +85,13 @@ describe('functionRefName matches the retired runtime', () => { }); describe('component references keep their raw path', () => { - it('matches what the retired runtime addressed the adapter by', () => { + it('addresses the adapter the way the retired runtime did', () => { const oursRef: Refs = createComponentRefs(); - const theirsRef: Refs = componentsGeneric(); - const path = ['betterAuth', 'adapter', 'findOne'] as const; - // The runtime's `getFunctionName` REFUSES a component reference (it has no - // name, only an address), which is why the shim reads the address itself. - expect(() => getFunctionName(walk(theirsRef, path))).toThrow(); - expect(functionRefName(walk(oursRef, path))).toBe( - '_reference/childComponent/betterAuth/adapter/findOne', - ); + // The retired runtime's `getFunctionName` REFUSED a component reference + // (it has no name, only an address), which is why the shim reads the + // address itself. + expect( + functionRefName(walk(oursRef, ['betterAuth', 'adapter', 'findOne'])), + ).toBe('_reference/childComponent/betterAuth/adapter/findOne'); }); }); diff --git a/services/platform/messages/de.yml b/services/platform/messages/de.yml index 9f67ffcbd8..853c918914 100644 --- a/services/platform/messages/de.yml +++ b/services/platform/messages/de.yml @@ -4242,9 +4242,6 @@ metadata: apiDocs: title: API-Dokumentation description: Die Tale-API mit interaktiver Dokumentation erkunden. - convexDashboard: - title: Convex Dashboard - description: Das Convex-Backend verwalten. knowledge: title: Wissen description: deine Wissensdatenbank für KI-Agents verwalten. diff --git a/services/platform/messages/en.yml b/services/platform/messages/en.yml index 258a8a3612..48a6b95b6a 100644 --- a/services/platform/messages/en.yml +++ b/services/platform/messages/en.yml @@ -4095,9 +4095,6 @@ metadata: apiDocs: title: API documentation description: Explore the Tale API with interactive documentation. - convexDashboard: - title: Convex Dashboard - description: Manage the Convex backend. knowledge: title: Knowledge description: Manage your knowledge base for AI agents. diff --git a/services/platform/messages/fr.yml b/services/platform/messages/fr.yml index f8407e4d13..45dda33f7d 100644 --- a/services/platform/messages/fr.yml +++ b/services/platform/messages/fr.yml @@ -4320,9 +4320,6 @@ metadata: apiDocs: title: Documentation API description: Explore l'API Tale avec la documentation interactive. - convexDashboard: - title: Convex Dashboard - description: Gère le backend Convex. knowledge: title: Connaissances description: Gère ta base de connaissances pour les agents IA. diff --git a/services/platform/package.json b/services/platform/package.json index a6be18e5d8..705dc3d8e5 100644 --- a/services/platform/package.json +++ b/services/platform/package.json @@ -35,7 +35,6 @@ "@better-auth/api-key": "1.6.23", "@better-auth/passkey": "1.6.23", "@casl/ability": "6.8.0", - "@convex-dev/better-auth": "0.12.2", "@dnd-kit/core": "6.3.1", "@dnd-kit/sortable": "10.0.0", "@dnd-kit/utilities": "3.2.2", @@ -83,7 +82,6 @@ "chokidar": "5.0.0", "class-variance-authority": "0.7.1", "clsx": "2.1.1", - "convex": "1.35.1", "cron-parser": "5.5.0", "date-fns": "4.1.0", "dayjs": "1.11.20", From 045744d24646184deb4c28eac96fdda679a89c91 Mon Sep 17 00:00:00 2001 From: larryro <371767072@qq.com> Date: Tue, 1 Sep 2026 10:34:43 +0800 Subject: [PATCH 4/5] docs(platform): record the convex teardown in the ledgers --- .agents/repo.md | 8 ++--- services/platform/README.md | 31 +++++++++---------- services/platform/backend/MIGRATION.md | 2 +- services/platform/backend/README.md | 41 +++++++++++--------------- services/platform/scripts/dev-modes.ts | 14 --------- 5 files changed, 37 insertions(+), 59 deletions(-) diff --git a/.agents/repo.md b/.agents/repo.md index 4732803a02..975e8c597a 100644 --- a/.agents/repo.md +++ b/.agents/repo.md @@ -22,14 +22,10 @@ Tale is a monorepo on Bun workspaces; every workspace script runs through ## Repo-specific boundaries -- **Never run `bun run setup:clean`** (wipes `services/platform/.convex/local/`) unless the user - explicitly asked to delete their local Convex dev data — automatic dev maintenance handles module - bloat; the clean script requires typing `delete local convex` on purpose. - **Org configuration is files, not tables** — per-org config is JSON/YAML under - `$TALE_CONFIG_DIR///` (Zod schemas in `lib/shared/schemas/`), never a Convex table - or DB row. + `$TALE_CONFIG_DIR///` (Zod schemas in `lib/shared/schemas/`), never a DB row. - **Tenant isolation — nothing org-owned is shared across organizations** — any new org-owned data - (a Convex table or field, an org config domain, a cache, a DB pool, an egress/browser-session + (a table or column, an org config domain, a cache, a DB pool, an egress/browser-session store, the RAG/crawler corpora `private_knowledge`/`public_web` + their embeddings) MUST be scoped and queried per organization. Per-org knowledge routing is `getKnowledgePoolForOrg(orgSlug)`, never the deployment-default `getKnowledgePool()`; introducing diff --git a/services/platform/README.md b/services/platform/README.md index 371d0a5836..e496804e1e 100644 --- a/services/platform/README.md +++ b/services/platform/README.md @@ -1,38 +1,40 @@ # @tale/platform -Tale's web application. Vite SPA with TanStack Start, served behind the proxy. - -## Overview - -Talks to the standalone `convex` service over the internal Docker network. Pushes Convex functions to that service via `bunx convex deploy` at startup; clients reach Convex through the proxy, not through this container. Calls `rag` and `crawler` for knowledge ingestion and retrieval. +Tale's web application and its backend. A Vite SPA (React 19 + TanStack +Router) served behind the proxy, plus the Postgres backend (`backend/` — a +Hono HTTP/SSE API and pg-boss job queues) that carries the whole product +surface. One image ships both: the web tier and the backend's `api` / `worker` +roles (role picked at boot). ## Interface Ports: -- `3000` — Vite app (static server) +- `3000` — Vite app (static server, `server.ts`) +- `3005` — backend HTTP/SSE API (`backend/main.ts`, `api`/`all` roles) Endpoints: - `GET /api/health` — JSON status, used by the proxy for blue-green health checks +- `/api/auth`, `/api/app`, `/api/v1`, `/events`, `/dav`, `/scim` — the backend's doors (see [backend/README.md](backend/README.md)) ## Configuration Notable variables (canonical list in `compose.yml`, which is local-dev only — production deployments use CLI-generated compose configs via `tale deploy`): - `HOST`, `PORT`, `LOG_LEVEL` -- `CONVEX_URL`, `CONVEX_DEPLOY_KEY` — point at the `convex` service +- `DATABASE_URL` — the backend's Postgres - `SANDBOX_URL` — internal DNS to the sandbox spawner - `KNOWLEDGE_DATABASE_URL` — knowledge corpus (ParadeDB) used by the in-process RAG/crawler path -- `INSTANCE_NAME`, `INSTANCE_SECRET` — used when generating Convex admin keys +- `INSTANCE_SECRET` — root secret the WebDAV HMAC and encryption keys derive from +- `TALE_CONFIG_DIR`, `TALE_CONFIG_BUILTIN_DIR`, `TALE_CONFIG_SYSTEM_DIR` — the file-based org-config trees ## Development ```bash docker compose up -d platform # via Compose (recommended) -bun run setup:check # pre-flight: Bun, Python, uv, ports, Convex CLI -bun run dev # default: spawns an ephemeral local Convex backend -CONVEX_EXTERNAL=true bun run dev # connects Vite to the convex container (docker compose up convex) +bun run setup:check # pre-flight: Bun, Node, Docker, ports +bun run dev # spawns the backend (+ its Postgres) and Vite together bun run check # format + lint + typecheck + tests ``` @@ -40,10 +42,9 @@ For prerequisites, the pre-flight check, and port-conflict handling, see the [co ## Layout -- `app/` — TanStack Start routes, features, and UI components -- `convex/` — Convex functions deployed to the `convex` service -- `lib/` — shared utilities, including TanStack DB collection infrastructure +- `app/` — TanStack Router routes, features, and UI components +- `backend/` — the Postgres backend: doors, jobs, auth, migrations; `backend/core/` is the ported domain logic the doors and jobs drive +- `lib/` — shared utilities (schemas, i18n, PII, harnesses, WebDAV protocol layer, …) - `messages/` — i18n message catalogues (`en.json` is the source of truth) - `scripts/` — operational helpers (see `scripts/README.md`) - `server.ts` — minimal HTTP shim wrapping the Vite static server with `/api/health` -- `generate-admin-key.sh` — generates Convex admin keys for the dashboard diff --git a/services/platform/backend/MIGRATION.md b/services/platform/backend/MIGRATION.md index b71ebf89bc..761c64035a 100644 --- a/services/platform/backend/MIGRATION.md +++ b/services/platform/backend/MIGRATION.md @@ -38,7 +38,7 @@ increment. | A blob ref resolves a file again | done | inc 144: clicking a task deliverable answered "Failed to load document" — the preview asked `GET /files//url` and got a 404. The app's file-identifier vocabulary is MIXED by contract, not by accident: listings hand out the row's own id, the POST upload lane's `storageId` IS the blob ref (its adapter says so), task deliverables carry the ref, and the 0.4 `getFileUrl` query took the ref and resolved the row by `storageId`. The 0.5 port kept only the row-id half: `/files/:fileId`, `/files/:fileId/url`, `/files/urls` and the delete all resolved `WHERE id = …`, so every ref-addressed read 404ed — while `/files/statuses` (ported separately) already resolved by ref, which is the tell that the vocabulary was mixed all along. One resolver closes it: `getFileMetadataByIdOrRef` — the `s3:` prefix makes the two identifiers unambiguous, and a ref is org-scoped twice over (the WHERE, then the key's own org prefix at presign). All four routes go through it. GUARD: the integration files check now round-trips the SAME upload through BOTH identifiers — the row id and the encoded ref — and the ref lane's body must match byte-for-byte. 260/260. | | The restart sidecar retires | done | inc 145: `services/controller/` is deleted — the opt-in docker-socket sidecar behind one-click "Apply & restart" on the data-residency page. Its lane was already BROKEN against 0.5: the app sent no `services`, the backend defaulted to `['convex']`, and the controller's own allowlist stopped accepting `convex` at inc 126 — so the button could only ever answer "invalid service" on a stack that enabled it, and nobody noticed, which is the tell that nobody used it. Its reason to exist was also gone: it was built so a deployment-config change could bounce `rag`+`convex` from the browser; both are dead, the surviving consumers of `deployment.json` are the backend tier and the sandbox spawner, and every operator who may EDIT deployment config is named in `TALE_DEPLOYMENT_CONFIG_ADMINS` — a host-side env var, so they have the host shell the manual path needs. The whole lane goes: `POST /api/app/deployment/restart` + `requestRestart` (backend), the wire-contract entry + adapter + hook + header button + confirm dialog (app; the post-save banner now shows `docker compose restart backend-api backend-worker` / `tale deploy`, replacing commands that named a deleted service), the `dataResidency.applyRestart*`/`restart*`/`restartConfirm*` strings (en/de/fr; de-CH had no overrides), the CLI's compose emitter + deploy bring-up + restore container candidate, the compose.yml service block (the stack's last compose profile dies with it), `scripts/dev.ts`'s sibling spawn, the `.env.example` CONTROLLER_* section, the CI build/release matrices and pull loops, the trivy ignore + commitlint scope, and the docs sections across en/de/fr (data-residency apply step, environment-reference section, compose-reference Profiles section, container-architecture + overview mentions, contributing-docker image row). cleanup-pr-images.yml deliberately KEEPS a `controller` row (like `convex`) so leftover PR image tags still get GC'd. The integration deployment probe drops its restart leg (an assertion, not a record): 260/260. | | The chat document vertical actually works | done | inc 146: a user asked the chat assistant to list their documents and got `column f.path does not exist` — and behind that first wall stood two more. (1) LISTING: both shim ports of the agent document listing selected `coalesce(d.folder_path, f.path)`, but `app.folders` has no `path` column — it is a name+parent_id tree — so Postgres rejected the whole statement and the chat `document` listing AND sandbox `document_find` (both doors) failed 100% since inc 126's cutover. The 0.4 semantics (folderPath = the row's own value, else the folder breadcrumb) port as ONE Postgres-native helper, `documents/agent-list.ts`: a root-down recursive CTE over the org's folders (depth-capped; a cycle never enters a root-down walk) joined onto `folder_id` — and the port's silently DROPPED `fileName`/`extension` filters (the sandbox bridge passes both) are restored, with the two doors' near-identical inline SQL collapsed into the helper ("two doors onto ONE helper" was the 0.4 contract the port had forked). (2) READ: `documents/internal_queries:findDocumentByFileId` was registered only in the SANDBOX shim map, which spreads the chat map one-way — so the chat `rag_fetch` inline fallback died as `[convex-shim] un-shimmed`; the handler moves to the chat map (sandbox inherits) and now also selects `content`, which BOTH doors' inline-document lane reads but the old handler never fetched. (3) INGEST: 0.4 indexed in 64-chunk slices because a Convex action has a budget — `indexDocument` commits one slice, answers `partial: true`, and the CALLER reschedules until the last slice stamps the corpus `completed`. The 0.5 `startRagIndexing` called it ONCE, ignored `partial`, and stamped the app row `completed` — so a 2.7MB PDF read "Indexed" after ten seconds with 64 of 936 chunks stored and the corpus row stuck `processing`, where BOTH corpus readers (keyword + dense filter on `status = 'completed'`) and the corpus-text read could never see it: a laundered success wearing a green badge. The worker owns the whole job in 0.5, so it drains the slices in-process — every slice stays committed and resumable (the plan resumes after the stored prefix), a zero-progress slice fails loud instead of spinning, and the per-slice `Embedding… N/total` progress write doubles as the watchdog's liveness signal (`IndexDocumentResult` gains `chunksStored` so the display is cumulative, unit-asserted). `skipped: 'unchanged'` — the corpus already holds ALL of this exact content — now maps to `completed`, not `failed`: a retry on an indexed document lands there. WHY NOTHING CAUGHT ANY OF IT: the chat shim's only exercised document leg was knowledge-entries; neither listing door, the read row, nor a document wider than one slice had ever run against real Postgres. GUARDS: the documents check gains an agent-listing leg (breadcrumb `Contracts/2026` through both doors, fileName narrows, bogus extension empties, the read row reachable through the CHAT map), and the RAG loop gains a multi-slice leg — a ~158KB upload that must reach `completed` with corpus chunks stored == planned > 64. The webdav check's `rag === 'queued'` assertion raced the worker's pickup and is widened to any live pipeline state (the property was always "the PUT entered the pipeline"). Proven end-to-end on a dev stack besides: the stuck 64/936 PDF re-indexed to 936/936 via retry-rag (the resume path), and the assistant then quoted § 90 BGB verbatim from page 43. 263/263. | -| `convex/` moves out and the rest is deleted | pending | The tree no longer needs a generator, a runtime, or the package's reference format (inc 136-137). `backend/` and `lib/` import nothing from `convex`; `convex/` imports only `v` / `Infer` / `GenericId` from `convex/values` — the validator vocabulary the reused handlers' argument contracts are written in, across 32 modules. TWO THINGS REMAIN. (1) Those validators become plain TypeScript (or zod, which the 0.5 doors already use) as each domain ports; `GenericId` in `lib/storage/blob_ref.ts` and `lib/type_cast_helpers.ts` is a branded string that 0.5 has no use for. (2) The reuse set moves out of `convex/` to a home named after what it is, and `convex`/`convex-helpers`/`@convex-dev/*` leave `package.json`. Order still matters — moving before the validators go would rewrite import paths in files that are about to change again. | +| `convex/` moves out and the rest is deleted | done | inc 146, in the order the last row prescribed. (1) The `convex/values` vocabulary became plain TypeScript: the 32 modules' `v.*`/`Infer` contracts are now type declarations (union/interface twins already sitting beside many of them), three files nobody imported (`sandbox/wire.ts`, `chat/schema.ts`, `enterprise_sso/validators.ts`) and the `validators.ts`/`schema.ts` shells whose only content was validators are deleted, `GenericId` is unbranded to `string` (nine `String()` casts fell out as dead conversions), and `type_cast_helpers`/`validators/json`/`lib/shared/schemas/utils/json-value` — casts to types that were `any` — are gone with their call sites inlined. The comment-only `CommentEventComment` build guard is REAL again (the backend's `comment.created` emit now types its reconstruction). (2) The tree moved to `backend/core/` — the ported domain logic the doors and jobs drive, beside `domains/` — with three genuinely shared server utilities relocated into `lib/` instead (`lib/net/safe-fetch.ts`, `lib/net/host-policy.ts`, `lib/chat/untrusted-content.ts`), so `lib/` imports nothing from `backend/`. A resolver-backed rewriter recomputed every relative/`@/` import (369 files) against the new layout; the app's `@/convex/*` type/constant imports became `@/backend/core/*`. `convex`, `@convex-dev/better-auth` (its client plugin was wired into `auth-client.ts` but its server half died with the runtime — along with the dead `convex-token-cache`/`use-auth-from-better-auth` bridge modules only their own tests still imported) and the `convex-helpers`/`@convex-dev/agent` patches left the manifests; `convex.json` is deleted; the function-refs proof suite keeps the expected strings and drops the package comparison, as designed. Build/config followed: Dockerfile (core copied pre-vite-build for the app's vocabulary imports; the runtime `/app/convex` copy, `convex.json`, and the dead `CONVEX_URL`/`INSTANCE_NAME` envs are gone), oxlint override globs, vitest excludes, the e2e cache key, the CLI's embedded reference tree (`convex` → `backend/core` label), and the stale WebDAV/wire/node-loader/README prose. `services/platform/convex/` no longer exists. | ## Porting rules (the constitution, enforced on every port) diff --git a/services/platform/backend/README.md b/services/platform/backend/README.md index 12f75c4f78..c037520e81 100644 --- a/services/platform/backend/README.md +++ b/services/platform/backend/README.md @@ -6,9 +6,9 @@ image**: the same image starts as an `api` container or a `worker` container (role picked at boot), replacing the separate Convex container. The default deployment runs one of each; either role scales horizontally on its own. 0.5 is a fresh instance (no data migration). This backend now carries the whole -product surface: the Convex service is deleted and the remaining `convex/` -tree holds only the 0.4 function layer the app's adapter seam still types -itself against — see `MIGRATION.md` for what is left to remove. +product surface. The domain logic ported from 0.4 lives in `core/` (driven +through the ctx-shim seams by the doors and jobs here); `MIGRATION.md` is the +campaign ledger that got it here. ## Constitution @@ -53,22 +53,20 @@ Sentry-compatible error reporting, errors only, no traces; see `error-reporting.ts`). Production runtime is **Node** (>= 22.18) running the `.ts` sources directly with `--experimental-transform-types` and the `node-loader.mjs` resolve hook — the -hook lets the backend import runtime-clean 0.4 modules (extensionless -specifiers under `../convex/**` and `../lib/**`) unchanged, so ports reuse -instead of fork-copying. Bun stays the package manager, build toolchain, and +hook lets the backend import the ported modules (extensionless specifiers +under `core/**` and `../lib/**`) unchanged, so ports reuse instead of +fork-copying. Bun stays the package manager, build toolchain, and dev runner for the rest of the workspace. Run locally: `bun run --filter @tale/platform backend:dev` (needs `DATABASE_URL`; `TALE_CONFIG_DIR`/`TALE_CONFIG_BUILTIN_DIR` for org-config reads and scaffolding). -To put the web app's dev server in front of it, start Vite with -`TALE_BACKEND_URL=http://127.0.0.1:` — `/api/auth`, `/api/app`, and -`/events` then proxy here while every other route keeps flowing to Convex -(the incremental-migration dev posture; see `vite.config.ts`). The app-side -data layer for these lanes lives in `app/lib/backend/` (fetch client, -`['backend', orgId, entity]` query keys, and the `/events` hint → -`invalidateQueries` hook). +`bun run dev` (repo root or this workspace) spawns this backend and the Vite +dev server together; Vite proxies `/api`, `/events`, `/dav`, and `/scim` here +(see `vite.config.ts`, `TALE_BACKEND_URL`). The app-side data layer lives in +`app/lib/backend/` (fetch client, `['backend', orgId, entity]` query keys, +and the `/events` hint → `invalidateQueries` hook). ## Tests @@ -126,14 +124,11 @@ data layer for these lanes lives in `app/lib/backend/` (fetch client, unqualified tables — they land in the first `search_path` schema (`tale` on the tale-db image). -## Ported surface (so far) +## Surface -`/api/app/*`: audit-logs, members, notifications, organizations, -user-preferences, users — see [MIGRATION.md](./MIGRATION.md) for the -domain-by-domain ledger and what each row still owes. - -## Deliberately not here yet - -SSO/trusted-headers/SCIM doors, 2FA org-enforcement hooks, crons, the -remaining domain ports (tracked in [MIGRATION.md](./MIGRATION.md)), proxy -routing, dev-loop integration. +Every product domain is served here — `/api/app/*`, the `/api/v1` REST +machine door, `/api/auth`, `/events`, `/dav`, `/scim`, webhooks, and the +pg-boss job lanes (automations, agent turns, sync, watchdogs, crons). +[MIGRATION.md](./MIGRATION.md) is the completed campaign ledger — the +domain-by-domain record of how each surface got here and the semantics it +carries. diff --git a/services/platform/scripts/dev-modes.ts b/services/platform/scripts/dev-modes.ts index e6df85e93c..685a4d8c77 100644 --- a/services/platform/scripts/dev-modes.ts +++ b/services/platform/scripts/dev-modes.ts @@ -27,17 +27,3 @@ export function shouldOpenBrowser( if (flag === undefined || flag.trim() === '') return true; return isTruthy(flag); } - -/** - * Adopt the backend endpoints allocated for THIS project from a - * freshly-parsed .env.local. On a fresh checkout the CLI writes only - * VITE_CONVEX_URL / VITE_CONVEX_SITE_URL (and CONVEX_DEPLOYMENT) — and when the - * default ports are taken (a second stack on one machine, e.g. an isolated E2E - * worktree beside `bun dev`) it picks OTHER ports. Everything that keys on - * CONVEX_URL / CONVEX_SITE_PROXY_URL (the readiness probe, the health check, - * the auth-route wait, the Vite proxy) falls back to :3210/:3211, so without - * this back-fill an isolated stack false-positives its probes against a - * NEIGHBOURING backend and silently proxies every /api and /ws_api request - * into it — test writes landing in the dev database. Explicit values win: - * only unset keys are filled. Pure given both env records. - */ From 730eba6c80ca71a3586c942f543dec66ff594a37 Mon Sep 17 00:00:00 2001 From: larryro <371767072@qq.com> Date: Tue, 1 Sep 2026 10:41:58 +0800 Subject: [PATCH 5/5] refactor(sandbox): drop the one-shot wire exports the teardown orphaned --- knip.config.ts | 16 ++++++---------- services/sandbox/src/types.ts | 18 ------------------ services/sandbox/src/wire.ts | 23 ++++------------------- 3 files changed, 10 insertions(+), 47 deletions(-) diff --git a/knip.config.ts b/knip.config.ts index 43d14ab6b5..6457aae806 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -53,16 +53,12 @@ export default { // Bun production server — invoked by docker-entrypoint.sh, not from // package.json scripts, so knip can't auto-detect it via the npm plugin. 'server.ts', - // Platform-only: Convex backend (separate runtime, not reachable via the - // SPA's import graph) and platform-specific app subtrees. - 'convex/**/*.ts', - '!convex/_generated/**', - '!convex/betterAuth/_generated/**', - // 0.5 Postgres backend — same shape as Convex: a separate Node runtime - // not reachable via the SPA's import graph. Domain files are the public - // surface (name-dispatched via the `internal.x.y.z` shim); knip cannot - // see that second graph, so they must be entries. Covers - // `backend/integration-check.ts` (invoked by `backend:integration`). + // Postgres backend — a separate Node runtime not reachable via the + // SPA's import graph. Domain files are the public surface + // (name-dispatched via the `internal.x.y.z` shim); knip cannot see + // that second graph, so they must be entries. Covers the ported + // logic under `backend/core/` and `backend/integration-check.ts` + // (invoked by `backend:integration`). 'backend/**/*.ts', 'app/features/**/*.{ts,tsx}', 'app/hooks/**/*.{ts,tsx}', diff --git a/services/sandbox/src/types.ts b/services/sandbox/src/types.ts index eb76b1a8e3..17525719d3 100644 --- a/services/sandbox/src/types.ts +++ b/services/sandbox/src/types.ts @@ -4,24 +4,6 @@ import type { RuntimeTier } from './runtime-tier.ts'; -/** - * Per-file harvest outcome. `storageId` is the Convex storage id allocated - * when the spawner POSTed the bytes to the pre-signed upload URL; the - * platform side just inserts the matching `fileMetadata` row. - * - * `sha256` (hex) is the digest of the raw bytes computed during harvest. - * Used for the cumulative `artifactOutputs` manifest (crispy-curry plan §1) - * and for pre-stage attestation when the same file is later re-injected - * into another run's `/agent/output/`. - */ -export interface OutputFile { - name: string; - storageId: string; - size: number; - contentType: string; - sha256: string; -} - export interface SpawnerConfig { // Execution backend (env SANDBOX_BACKEND). 'docker' spawns sibling // containers via the host docker socket (Compose, the default); diff --git a/services/sandbox/src/wire.ts b/services/sandbox/src/wire.ts index 837cdb46a2..dbc97bdcfc 100644 --- a/services/sandbox/src/wire.ts +++ b/services/sandbox/src/wire.ts @@ -56,25 +56,10 @@ export const sandboxErrorCodeLiterals = [ export type SandboxErrorCode = (typeof sandboxErrorCodeLiterals)[number]; -/** - * SSE event types emitted by `POST /v1/execute`. The spawner emits: - * - `phase` — zero or more transitions (preparing → installing → running) - * - `stdout` / `stderr` — incremental output deltas while the container - * is alive (added so the canvas can tail output instead of waiting for - * the terminal `result` event with the whole base64'd buffer). - * - `result` — exactly one terminal event with the canonical - * ExecuteResponse shape. - * - `error` — zero or one SSE-side transport error (e.g. spawn aborted - * before a result was produced). - * - */ -export const sandboxSseEventLiterals = [ - 'phase', - 'stdout', - 'stderr', - 'result', - 'error', -] as const; +// The session SSE event names (`phase` / `stdout` / `stderr` / `result` / +// `error`) live at their emission sites in `session/session-routes.ts`, +// pinned by that module's tests — the one-shot `/v1/execute` lane that once +// declared them here as a vocabulary is retired. // Stable id alphabet for executionId (Convex doc id + base32-ish dev ids). // Used by both the server route regex and the spawn-time argv assertions.