spine-framework 0.3.125 → 0.3.127
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.framework/cli/commands/agents.ts +35 -37
- package/.framework/cli/commands/auth.ts +35 -34
- package/.framework/cli/commands/create-app.ts +104 -40
- package/.framework/cli/commands/dev.ts +104 -69
- package/.framework/cli/commands/doctor.ts +169 -97
- package/.framework/cli/commands/generate.ts +82 -161
- package/.framework/cli/commands/init.ts +72 -34
- package/.framework/cli/commands/install-app.ts +260 -125
- package/.framework/cli/commands/items.ts +33 -44
- package/.framework/cli/commands/migrate.ts +122 -40
- package/.framework/cli/commands/migrations.ts +33 -40
- package/.framework/cli/commands/pipelines.ts +31 -39
- package/.framework/cli/commands/status.ts +47 -23
- package/.framework/cli/commands/system.ts +47 -37
- package/.framework/cli/commands/test.ts +112 -68
- package/.framework/cli/commands/validate-app.ts +215 -121
- package/.framework/cli/context.ts +112 -130
- package/.framework/cli/env-loader.ts +47 -18
- package/.framework/cli/index.ts +81 -42
- package/.framework/docs/runtime-types.md +1024 -0
- package/.framework/functions/_shared/agent-runner.ts +1296 -961
- package/.framework/functions/_shared/app-manifest.ts +82 -68
- package/.framework/functions/_shared/audit.ts +135 -83
- package/.framework/functions/_shared/{cil-state.ts → cil.ts} +147 -54
- package/.framework/functions/_shared/db.ts +80 -126
- package/.framework/functions/_shared/hash-api-key.ts +63 -0
- package/.framework/functions/_shared/index.ts +222 -347
- package/.framework/functions/_shared/kb.ts +319 -0
- package/.framework/functions/_shared/llm.ts +92 -0
- package/.framework/functions/_shared/logging.ts +288 -0
- package/.framework/functions/_shared/middleware.ts +329 -365
- package/.framework/functions/_shared/outcomes.ts +36 -0
- package/.framework/functions/_shared/permissions.ts +359 -643
- package/.framework/functions/_shared/pipeline-runner.ts +243 -316
- package/.framework/functions/_shared/principal.ts +376 -468
- package/.framework/functions/_shared/resolve-credentials.ts +102 -0
- package/.framework/functions/_shared/resolve-ids.ts +162 -14
- package/.framework/functions/_shared/retry-deliveries.ts +104 -0
- package/.framework/functions/_shared/schema-utils.ts +611 -373
- package/.framework/functions/_shared/testing.ts +207 -137
- package/.framework/functions/_shared/tracing.ts +276 -0
- package/.framework/functions/_shared/trigger-engine.ts +159 -211
- package/.framework/functions/_shared/webhook-registration.ts +95 -81
- package/.framework/functions/_shared/webhook-registry.ts +153 -75
- package/.framework/functions/account-nodes.ts +31 -53
- package/.framework/functions/adapters/generic-webhook-inbound.ts +56 -0
- package/.framework/functions/adapters/generic-webhook-outbound.ts +43 -0
- package/.framework/functions/adapters/openai-outbound.ts +40 -0
- package/.framework/functions/admin-data.ts +687 -533
- package/.framework/functions/admin-funnel.ts +31 -20
- package/.framework/functions/admin-install-funnel.ts +47 -28
- package/.framework/functions/admin-signals.ts +47 -29
- package/.framework/functions/ai-agents.ts +122 -147
- package/.framework/functions/ai-interactions.ts +180 -90
- package/.framework/functions/api-keys.ts +250 -192
- package/.framework/functions/apps.ts +203 -197
- package/.framework/functions/auth.ts +98 -91
- package/.framework/functions/cil-agent-ingest.ts +773 -0
- package/.framework/functions/cil-agent.ts +592 -0
- package/.framework/functions/cil-callmap.ts +1227 -0
- package/.framework/functions/cil-config.ts +583 -0
- package/.framework/functions/cil-context.ts +445 -0
- package/.framework/functions/cil-doc.ts +1752 -0
- package/.framework/functions/cil-embeddings.ts +1202 -0
- package/.framework/functions/cil-graph.ts +585 -0
- package/.framework/functions/cil-ingest.ts +2416 -0
- package/.framework/functions/cil-pipeline.ts +318 -0
- package/.framework/functions/cil-project.ts +499 -0
- package/.framework/functions/cil-validate.ts +469 -0
- package/.framework/functions/debug-auth.ts +73 -53
- package/.framework/functions/deliveries.ts +117 -0
- package/.framework/functions/embeddings.ts +528 -516
- package/.framework/functions/eval-harness.ts +158 -106
- package/.framework/functions/integration-outbound.ts +311 -0
- package/.framework/functions/integration-routes.ts +240 -466
- package/.framework/functions/integrations.ts +132 -140
- package/.framework/functions/invites.ts +401 -201
- package/.framework/functions/item-progress.ts +113 -53
- package/.framework/functions/kb.ts +28 -0
- package/.framework/functions/logs.ts +239 -222
- package/.framework/functions/observability.ts +308 -259
- package/.framework/functions/pipeline-executions.ts +179 -235
- package/.framework/functions/pipelines.ts +163 -205
- package/.framework/functions/prompt-configs.ts +109 -156
- package/.framework/functions/roles.ts +97 -147
- package/.framework/functions/system-cron.ts +132 -218
- package/.framework/functions/system.ts +112 -52
- package/.framework/functions/tests.ts +32 -28
- package/.framework/functions/timers.ts +162 -210
- package/.framework/functions/triggers.ts +282 -346
- package/.framework/functions/types.ts +283 -400
- package/.framework/migrations/001_core_schema.sql +630 -0
- package/.framework/migrations/002_core_rls_and_functions.sql +532 -0
- package/.framework/migrations/{003_system_data.sql → 003_core_system_data.sql} +26 -153
- package/.framework/migrations/004_core_rpc_and_broker.sql +421 -0
- package/.framework/migrations/005_core_observability.sql +598 -0
- package/.framework/migrations/006_security_hardening_rls.sql +97 -0
- package/.framework/migrations/007_kb_article_list.sql +237 -0
- package/.framework/migrations/008_items_target.sql +8 -0
- package/.framework/migrations/009_roles_slug_unique.sql +11 -0
- package/.framework/public/spine.config.json +91 -0
- package/.framework/scripts/app-install-cli.ts +112 -98
- package/.framework/src/App.tsx +67 -20
- package/.framework/src/apps/admin/index.tsx +97 -27
- package/.framework/src/components/AppWrapper.tsx +50 -11
- package/.framework/src/components/CustomAppLoader.tsx +131 -22
- package/.framework/src/components/admin/AdminListPage.tsx +75 -8
- package/.framework/src/components/admin/AdminSidebar.tsx +148 -36
- package/.framework/src/components/admin/AdminStatsCard.tsx +48 -29
- package/.framework/src/components/admin/PageInstructions.tsx +49 -0
- package/.framework/src/components/admin/SortableTableHeader.tsx +49 -2
- package/.framework/src/components/app-shell/GenericAppShell.tsx +69 -11
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +55 -10
- package/.framework/src/components/app-shell/GenericListPage.tsx +76 -9
- package/.framework/src/components/app-sidebar.tsx +50 -3
- package/.framework/src/components/auth/ProtectedRoute.tsx +53 -45
- package/.framework/src/components/kb/KBArticleEditor.tsx +409 -0
- package/.framework/src/components/kb/KBArticleList.tsx +557 -0
- package/.framework/src/components/layout/AppShell.tsx +54 -16
- package/.framework/src/components/layout/Header.tsx +50 -38
- package/.framework/src/components/layout/Layout.tsx +48 -32
- package/.framework/src/components/layout/Sidebar.tsx +167 -16
- package/.framework/src/components/runtime/DataDetailHeader.tsx +33 -1
- package/.framework/src/components/runtime/DataDetailPage.tsx +289 -4
- package/.framework/src/components/runtime/DataFilters.tsx +78 -1
- package/.framework/src/components/runtime/DataHeader.tsx +32 -29
- package/.framework/src/components/runtime/DataListPage.tsx +161 -2
- package/.framework/src/components/runtime/DataStats.tsx +48 -1
- package/.framework/src/components/runtime/DataTable.tsx +153 -1
- package/.framework/src/components/runtime/SchemaDetailForm.tsx +77 -46
- package/.framework/src/components/runtime/index.ts +16 -9
- package/.framework/src/components/search-form.tsx +33 -1
- package/.framework/src/components/shared/AgentView.tsx +48 -51
- package/.framework/src/components/shared/FieldRenderer.tsx +84 -67
- package/.framework/src/components/shared/InviteManager.tsx +113 -52
- package/.framework/src/components/shared/MemberList.tsx +182 -0
- package/.framework/src/components/shared/SchemaFields.tsx +96 -78
- package/.framework/src/components/ui/DataTable.tsx +86 -56
- package/.framework/src/components/ui/Form.tsx +188 -109
- package/.framework/src/components/ui/ItemCard.tsx +86 -58
- package/.framework/src/components/ui/ItemListView.tsx +88 -61
- package/.framework/src/components/ui/LoadingSpinner.tsx +52 -27
- package/.framework/src/components/ui/Modal.tsx +81 -4
- package/.framework/src/components/ui/RichTextEditor.tsx +101 -9
- package/.framework/src/components/ui/accordion.tsx +97 -5
- package/.framework/src/components/ui/alert-dialog.tsx +225 -13
- package/.framework/src/components/ui/alert.tsx +113 -6
- package/.framework/src/components/ui/aspect-ratio.tsx +49 -2
- package/.framework/src/components/ui/avatar.tsx +129 -7
- package/.framework/src/components/ui/badge.tsx +65 -3
- package/.framework/src/components/ui/breadcrumb.tsx +145 -8
- package/.framework/src/components/ui/button-group.tsx +81 -4
- package/.framework/src/components/ui/button.tsx +49 -2
- package/.framework/src/components/ui/calendar.tsx +65 -3
- package/.framework/src/components/ui/card.tsx +145 -8
- package/.framework/src/components/ui/carousel.tsx +129 -7
- package/.framework/src/components/ui/chart.tsx +161 -10
- package/.framework/src/components/ui/checkbox.tsx +49 -2
- package/.framework/src/components/ui/collapsible.tsx +81 -5
- package/.framework/src/components/ui/combobox.tsx +305 -20
- package/.framework/src/components/ui/command.tsx +177 -11
- package/.framework/src/components/ui/context-menu.tsx +273 -17
- package/.framework/src/components/ui/dialog.tsx +209 -27
- package/.framework/src/components/ui/direction.tsx +64 -4
- package/.framework/src/components/ui/drawer.tsx +193 -12
- package/.framework/src/components/ui/dropdown-menu.tsx +273 -18
- package/.framework/src/components/ui/empty.tsx +145 -9
- package/.framework/src/components/ui/field.tsx +209 -14
- package/.framework/src/components/ui/hover-card.tsx +81 -4
- package/.framework/src/components/ui/input-group.tsx +162 -10
- package/.framework/src/components/ui/input-otp.tsx +98 -5
- package/.framework/src/components/ui/input.tsx +49 -2
- package/.framework/src/components/ui/item.tsx +225 -13
- package/.framework/src/components/ui/kbd.tsx +65 -3
- package/.framework/src/components/ui/label.tsx +49 -2
- package/.framework/src/components/ui/menubar.tsx +289 -17
- package/.framework/src/components/ui/native-select.tsx +97 -5
- package/.framework/src/components/ui/navigation-menu.tsx +177 -10
- package/.framework/src/components/ui/pagination.tsx +161 -9
- package/.framework/src/components/ui/popover.tsx +145 -8
- package/.framework/src/components/ui/progress.tsx +49 -2
- package/.framework/src/components/ui/radio-group.tsx +65 -3
- package/.framework/src/components/ui/resizable.tsx +82 -4
- package/.framework/src/components/ui/scroll-area.tsx +65 -3
- package/.framework/src/components/ui/select.tsx +193 -11
- package/.framework/src/components/ui/separator.tsx +49 -2
- package/.framework/src/components/ui/sheet.tsx +193 -11
- package/.framework/src/components/ui/sidebar.tsx +481 -38
- package/.framework/src/components/ui/skeleton.tsx +49 -2
- package/.framework/src/components/ui/slider.tsx +49 -2
- package/.framework/src/components/ui/sonner.tsx +49 -2
- package/.framework/src/components/ui/spinner.tsx +49 -2
- package/.framework/src/components/ui/switch.tsx +49 -2
- package/.framework/src/components/ui/table-primitives.tsx +160 -33
- package/.framework/src/components/ui/table.tsx +161 -9
- package/.framework/src/components/ui/tabs.tsx +113 -6
- package/.framework/src/components/ui/textarea.tsx +49 -2
- package/.framework/src/components/ui/toggle-group.tsx +81 -4
- package/.framework/src/components/ui/toggle.tsx +65 -3
- package/.framework/src/components/ui/tooltip.tsx +97 -5
- package/.framework/src/contexts/AppContext.tsx +129 -45
- package/.framework/src/contexts/AuthContext.tsx +173 -145
- package/.framework/src/hooks/use-mobile.ts +48 -2
- package/.framework/src/hooks/useApi.ts +208 -159
- package/.framework/src/hooks/useAppPath.ts +32 -23
- package/.framework/src/hooks/useApps.ts +51 -20
- package/.framework/src/hooks/useEntityList.ts +32 -78
- package/.framework/src/hooks/useEntityRecord.ts +35 -96
- package/.framework/src/hooks/useForm.ts +32 -92
- package/.framework/src/hooks/useListSchema.ts +49 -78
- package/.framework/src/hooks/useSchemaRecord.ts +114 -105
- package/.framework/src/lib/api.ts +192 -115
- package/.framework/src/lib/supabase.ts +98 -64
- package/.framework/src/lib/utils.ts +287 -151
- package/.framework/src/main.tsx +49 -3
- package/.framework/src/pages/DashboardPage.tsx +52 -29
- package/.framework/src/pages/NotFoundPage.tsx +32 -10
- package/.framework/src/pages/SetupPage.tsx +32 -12
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +119 -10
- package/.framework/src/pages/admin/AIAgentsPage.tsx +48 -19
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +187 -17
- package/.framework/src/pages/admin/APIKeysPage.tsx +98 -22
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +113 -26
- package/.framework/src/pages/admin/AppDetailPage.tsx +125 -8
- package/.framework/src/pages/admin/AppsPage.tsx +49 -2
- package/.framework/src/pages/admin/DeliveriesPage.tsx +196 -0
- package/.framework/src/pages/admin/DesignedPage.tsx +32 -18
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +215 -484
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +116 -319
- package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +34 -9
- package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +34 -9
- package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -16
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +183 -26
- package/.framework/src/pages/admin/IntegrationsPage.tsx +48 -19
- package/.framework/src/pages/admin/InvitesPage.tsx +48 -14
- package/.framework/src/pages/admin/LogsPage.tsx +422 -75
- package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +34 -9
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +113 -24
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +206 -34
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +50 -17
- package/.framework/src/pages/admin/PipelinesPage.tsx +160 -21
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +378 -88
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +125 -16
- package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +49 -10
- package/.framework/src/pages/admin/RoleDetailPage.tsx +130 -10
- package/.framework/src/pages/admin/RolesPage.tsx +171 -18
- package/.framework/src/pages/admin/SelectTestPage.tsx +34 -9
- package/.framework/src/pages/admin/ShadcnTestPage.tsx +49 -11
- package/.framework/src/pages/admin/SimpleDashboard.tsx +49 -10
- package/.framework/src/pages/admin/TestRunDetailPage.tsx +49 -4
- package/.framework/src/pages/admin/TestingDashboard.tsx +98 -28
- package/.framework/src/pages/admin/TimerDetailPage.tsx +143 -33
- package/.framework/src/pages/admin/TimersPage.tsx +48 -20
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +118 -30
- package/.framework/src/pages/admin/TriggersPage.tsx +64 -19
- package/.framework/src/pages/admin/TypeDetailPage.tsx +138 -15
- package/.framework/src/pages/admin/TypesPage.tsx +48 -20
- package/.framework/src/pages/admin/cil/CILAgentPage.tsx +465 -0
- package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +787 -0
- package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +699 -0
- package/.framework/src/pages/admin/cil/CILFileListPage.tsx +647 -0
- package/.framework/src/pages/admin/cil/CILGraphPage.tsx +616 -0
- package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +1402 -0
- package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +614 -0
- package/.framework/src/pages/admin/cil/CILSearchPage.tsx +571 -0
- package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +731 -0
- package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
- package/.framework/src/pages/auth/AuthConfirmPage.tsx +52 -51
- package/.framework/src/pages/auth/ForgotPasswordPage.tsx +31 -22
- package/.framework/src/pages/auth/LoginPage.tsx +50 -23
- package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -15
- package/.framework/src/pages/auth/RegisterPage.tsx +281 -317
- package/.framework/src/pages/spine-framework/APIPage.tsx +33 -0
- package/.framework/src/pages/spine-framework/CLIPage.tsx +33 -0
- package/.framework/src/types/auth.ts +128 -71
- package/.framework/src/types/types.ts +373 -157
- package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +39 -0
- package/custom/.devin/skills/spine-workbench-idea/SKILL.md +36 -0
- package/custom/.devin/skills/spine-workbench-implement/SKILL.md +46 -0
- package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +28 -0
- package/custom/.devin/skills/spine-workbench-plan/SKILL.md +38 -0
- package/docs/README.md +9 -130
- package/netlify.toml +3 -3
- package/package.json +13 -25
- package/scripts/kb-ingest-docs.ts +203 -0
- package/spine.config.json +65 -4
- package/.framework/migrations/001_schema.sql +0 -425
- package/.framework/migrations/002_rls_and_functions.sql +0 -763
- package/.framework/migrations/004_enhancements.sql +0 -43
- package/.framework/migrations/005_auth_user_trigger.sql +0 -105
- package/.framework/migrations/006_fix_current_actor_id.sql +0 -21
- package/.framework/migrations/016_invites.sql +0 -27
- package/.framework/migrations/017_fix_get_account_apps.sql +0 -78
- package/.framework/migrations/018_apps_required_roles.sql +0 -17
- package/.framework/migrations/019_triggers_unique_constraint.sql +0 -13
- package/.framework/migrations/020_get_account_apps_role_filter.sql +0 -117
- package/.framework/migrations/021_app_domains.sql +0 -112
- package/.framework/migrations/022_route_prefix_domain_unique.sql +0 -21
- package/.framework/migrations/023_match_embeddings_rpc.sql +0 -78
- package/.framework/migrations/024_messages_account_id.sql +0 -9
- package/.framework/migrations/025_retype_threads_messages.sql +0 -240
- package/.framework/migrations/026_fix_rls_policies.sql +0 -47
- package/.framework/migrations/027_embedding_lifecycle.sql +0 -176
- package/.framework/migrations/028_fix_message_data_permissions.sql +0 -49
- package/.framework/migrations/029_tighten_support_triage_prompt.sql +0 -41
- package/.framework/migrations/030_messages_updated_by.sql +0 -6
- package/.framework/migrations/031_seed_upsert_constraints.sql +0 -35
- package/docs/APPS.md +0 -232
- package/docs/ARCHITECTURE.md +0 -246
- package/docs/INSTALL.md +0 -155
- package/docs/design-schema-spec.md +0 -1837
- package/docs/dev-guides/01-create-an-app.md +0 -183
- package/docs/dev-guides/02-create-a-function.md +0 -178
- package/docs/dev-guides/03-create-a-component.md +0 -178
- package/docs/dev-guides/04-webhook-handlers.md +0 -129
- package/docs/dev-guides/05-testing.md +0 -168
- package/docs/dev-guides/README.md +0 -40
- package/docs/npm-packages-plan.md +0 -288
- package/docs/spine-core--api-reference.md +0 -822
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { adminDb } from './db'
|
|
2
|
+
import type { CoreContext } from './middleware'
|
|
3
|
+
import { sanitizeRecordData } from './permissions'
|
|
4
|
+
import { callOpenAIEmbeddings } from './llm'
|
|
5
|
+
|
|
6
|
+
const EMBEDDING_MODEL = 'text-embedding-3-small'
|
|
7
|
+
const DEFAULT_CHUNK_SIZE = 800
|
|
8
|
+
const DEFAULT_CHUNK_OVERLAP = 100
|
|
9
|
+
const SIMILARITY_THRESHOLD = 0.15
|
|
10
|
+
const DEFAULT_MATCH_COUNT = 10
|
|
11
|
+
const SECURITY_LEVELS: Record<string, string> = {
|
|
12
|
+
'kb-spine-admin': 'restricted',
|
|
13
|
+
'kb-spine-internal': 'internal',
|
|
14
|
+
'kb-spine-public': 'external',
|
|
15
|
+
'kb-account': 'external',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function generateEmbedding(text: string, ctx?: CoreContext): Promise<number[]> {
|
|
19
|
+
const json = await callOpenAIEmbeddings(ctx, [text], EMBEDDING_MODEL)
|
|
20
|
+
return json.data?.[0]?.embedding || []
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function chunkText(text: string, chunkSize: number, overlap: number): string[] {
|
|
24
|
+
if (!text) return []
|
|
25
|
+
if (text.length <= chunkSize) return [text]
|
|
26
|
+
|
|
27
|
+
const chunks: string[] = []
|
|
28
|
+
let start = 0
|
|
29
|
+
while (start < text.length) {
|
|
30
|
+
const end = Math.min(start + chunkSize, text.length)
|
|
31
|
+
const chunk = text.slice(start, end)
|
|
32
|
+
chunks.push(chunk)
|
|
33
|
+
start += chunkSize - overlap
|
|
34
|
+
if (start >= end) break
|
|
35
|
+
}
|
|
36
|
+
return chunks
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getFieldValue(record: any, field: string): string {
|
|
40
|
+
if (record[field] !== undefined && record[field] !== null) {
|
|
41
|
+
const value = record[field]
|
|
42
|
+
if (Array.isArray(value)) return value.join(' ')
|
|
43
|
+
return String(value)
|
|
44
|
+
}
|
|
45
|
+
const data = record.data || {}
|
|
46
|
+
if (data[field] !== undefined && data[field] !== null) {
|
|
47
|
+
const value = data[field]
|
|
48
|
+
if (Array.isArray(value)) return value.join(' ')
|
|
49
|
+
return String(value)
|
|
50
|
+
}
|
|
51
|
+
return ''
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getRawFieldValue(record: any, field: string): any {
|
|
55
|
+
if (record[field] !== undefined && record[field] !== null) return record[field]
|
|
56
|
+
const data = record.data || {}
|
|
57
|
+
if (data[field] !== undefined && data[field] !== null) return data[field]
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function matchesFilters(record: any, filters: Record<string, any>): boolean {
|
|
62
|
+
if (!filters || Object.keys(filters).length === 0) return true
|
|
63
|
+
|
|
64
|
+
for (const [field, expected] of Object.entries(filters)) {
|
|
65
|
+
const raw = getRawFieldValue(record, field)
|
|
66
|
+
const actual = Array.isArray(raw) ? raw : (raw !== undefined ? [raw] : [])
|
|
67
|
+
const expectedArr = Array.isArray(expected) ? expected : (expected !== undefined ? [expected] : [])
|
|
68
|
+
if (expectedArr.length === 0) continue
|
|
69
|
+
|
|
70
|
+
const hasOverlap = expectedArr.some((ev: any) => actual.includes(ev))
|
|
71
|
+
if (!hasOverlap) return false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function ensurePlatformAccountId(): Promise<string | null> {
|
|
78
|
+
const { data } = await adminDb
|
|
79
|
+
.from('accounts')
|
|
80
|
+
.select('id')
|
|
81
|
+
.eq('slug', 'spine-system')
|
|
82
|
+
.maybeSingle()
|
|
83
|
+
return data?.id ?? null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function kbIngest(ctx: CoreContext, body: { id: string }): Promise<{ ok: boolean; message: string; count?: number }> {
|
|
87
|
+
const { id } = body
|
|
88
|
+
if (!id) throw new Error('id is required')
|
|
89
|
+
|
|
90
|
+
const { data: record, error } = await adminDb
|
|
91
|
+
.from('items')
|
|
92
|
+
.select('*')
|
|
93
|
+
.eq('id', id)
|
|
94
|
+
.eq('account_id', ctx.accountId)
|
|
95
|
+
.maybeSingle()
|
|
96
|
+
|
|
97
|
+
if (error) throw error
|
|
98
|
+
if (!record) throw new Error('record not found')
|
|
99
|
+
|
|
100
|
+
if (record.status !== 'published') {
|
|
101
|
+
await kbRevoke(ctx, { id })
|
|
102
|
+
return { ok: true, message: 'record is not published; embeddings revoked if any' }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const designSchema = record.design_schema || {}
|
|
106
|
+
const kbConfig = designSchema.functionality?.kb || {}
|
|
107
|
+
const vectorFields = kbConfig.vector_fields || ['title', 'content']
|
|
108
|
+
const chunkField = kbConfig.chunk_field || 'content'
|
|
109
|
+
const chunkSize = kbConfig.chunk_size || DEFAULT_CHUNK_SIZE
|
|
110
|
+
const chunkOverlap = kbConfig.chunk_overlap || DEFAULT_CHUNK_OVERLAP
|
|
111
|
+
|
|
112
|
+
const { data: typeRecord, error: typeSlugError } = await adminDb
|
|
113
|
+
.from('types')
|
|
114
|
+
.select('slug, app_id')
|
|
115
|
+
.eq('id', record.type_id)
|
|
116
|
+
.single()
|
|
117
|
+
if (typeSlugError) throw typeSlugError
|
|
118
|
+
|
|
119
|
+
const typeSlug = typeRecord?.slug || ''
|
|
120
|
+
const appId = record.app_id || typeRecord?.app_id || null
|
|
121
|
+
const securityLevel = SECURITY_LEVELS[typeSlug] || 'internal'
|
|
122
|
+
|
|
123
|
+
await kbRevoke(ctx, { id })
|
|
124
|
+
|
|
125
|
+
const embeddingsToInsert: any[] = []
|
|
126
|
+
let chunkIndex = 0
|
|
127
|
+
|
|
128
|
+
for (const field of vectorFields) {
|
|
129
|
+
const raw = getFieldValue(record, field)
|
|
130
|
+
if (!raw.trim()) continue
|
|
131
|
+
|
|
132
|
+
const isChunked = field === chunkField && raw.length > chunkSize
|
|
133
|
+
const chunks = isChunked ? chunkText(raw, chunkSize, chunkOverlap) : [raw]
|
|
134
|
+
|
|
135
|
+
for (const chunk of chunks) {
|
|
136
|
+
if (!chunk.trim()) continue
|
|
137
|
+
const embedding = await generateEmbedding(chunk, ctx)
|
|
138
|
+
if (!embedding.length) continue
|
|
139
|
+
|
|
140
|
+
embeddingsToInsert.push({
|
|
141
|
+
account_id: record.account_id,
|
|
142
|
+
model_id: EMBEDDING_MODEL,
|
|
143
|
+
document_id: id,
|
|
144
|
+
chunk_index: chunkIndex++,
|
|
145
|
+
content: chunk,
|
|
146
|
+
embedding: JSON.stringify(embedding),
|
|
147
|
+
metadata: {
|
|
148
|
+
type_slug: typeSlug,
|
|
149
|
+
item_type: typeSlug,
|
|
150
|
+
app_id: appId,
|
|
151
|
+
vector_type: field,
|
|
152
|
+
security_level: securityLevel,
|
|
153
|
+
status: record.status,
|
|
154
|
+
},
|
|
155
|
+
approved_at: new Date().toISOString(),
|
|
156
|
+
revoked_at: null,
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (embeddingsToInsert.length === 0) {
|
|
162
|
+
return { ok: true, message: 'no content to embed', count: 0 }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { error: insertError } = await adminDb.from('embeddings').insert(embeddingsToInsert)
|
|
166
|
+
if (insertError) throw insertError
|
|
167
|
+
|
|
168
|
+
return { ok: true, message: 'embeddings generated', count: embeddingsToInsert.length }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function kbRevoke(ctx: CoreContext, body: { id: string }): Promise<{ ok: boolean; message: string; count?: number }> {
|
|
172
|
+
const { id } = body
|
|
173
|
+
if (!id) throw new Error('id is required')
|
|
174
|
+
|
|
175
|
+
const { data, error } = await adminDb.rpc('revoke_embeddings_for_item', { p_item_id: id })
|
|
176
|
+
if (error) throw error
|
|
177
|
+
|
|
178
|
+
const { error: deleteError } = await adminDb
|
|
179
|
+
.from('embeddings')
|
|
180
|
+
.delete()
|
|
181
|
+
.eq('document_id', id)
|
|
182
|
+
|
|
183
|
+
if (deleteError) throw deleteError
|
|
184
|
+
|
|
185
|
+
return { ok: true, message: 'embeddings revoked', count: data || 0 }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function kbReindex(ctx: CoreContext, body: { type_slug: string; app_id?: string }): Promise<{ ok: boolean; count: number }> {
|
|
189
|
+
const { type_slug } = body
|
|
190
|
+
if (!type_slug) throw new Error('type_slug is required')
|
|
191
|
+
|
|
192
|
+
const { data: typeRecord } = await adminDb
|
|
193
|
+
.from('types')
|
|
194
|
+
.select('id')
|
|
195
|
+
.eq('slug', type_slug)
|
|
196
|
+
.maybeSingle()
|
|
197
|
+
|
|
198
|
+
if (!typeRecord) throw new Error(`type not found: ${type_slug}`)
|
|
199
|
+
|
|
200
|
+
let query = adminDb.from('items').select('id').eq('type_id', typeRecord.id).eq('status', 'published')
|
|
201
|
+
if (body.app_id) query = query.eq('app_id', body.app_id)
|
|
202
|
+
|
|
203
|
+
const { data: records, error } = await query
|
|
204
|
+
if (error) throw error
|
|
205
|
+
|
|
206
|
+
let count = 0
|
|
207
|
+
for (const record of records || []) {
|
|
208
|
+
await kbIngest(ctx, { id: record.id })
|
|
209
|
+
count++
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { ok: true, count }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function kbSearch(ctx: CoreContext, body: any): Promise<{ results: any[]; count: number }> {
|
|
216
|
+
const query = body.query
|
|
217
|
+
const typeSlugs = body.type_slugs || []
|
|
218
|
+
const sources = body.sources || ['kb']
|
|
219
|
+
const appId = body.app_id || null
|
|
220
|
+
const vectorFields = body.vector_fields || []
|
|
221
|
+
const limit = body.limit || DEFAULT_MATCH_COUNT
|
|
222
|
+
const offset = body.offset || 0
|
|
223
|
+
const filters = body.filters || {}
|
|
224
|
+
const threshold = typeof body.similarity_threshold === 'number' ? body.similarity_threshold : SIMILARITY_THRESHOLD
|
|
225
|
+
|
|
226
|
+
if (!query) throw new Error('query is required')
|
|
227
|
+
|
|
228
|
+
const accountIds = [ctx.accountId]
|
|
229
|
+
const platformAccountId = await ensurePlatformAccountId()
|
|
230
|
+
if (platformAccountId && platformAccountId !== ctx.accountId) {
|
|
231
|
+
accountIds.push(platformAccountId)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const queryEmbedding = await generateEmbedding(query, ctx)
|
|
235
|
+
if (!queryEmbedding.length) return { results: [], count: 0 }
|
|
236
|
+
|
|
237
|
+
const allowedVectorTypes = new Set(vectorFields)
|
|
238
|
+
const allowedTypeSlugs = new Set(typeSlugs)
|
|
239
|
+
const includeKb = sources.includes('kb')
|
|
240
|
+
const includeCode = sources.includes('code')
|
|
241
|
+
|
|
242
|
+
const allMatches: any[] = []
|
|
243
|
+
const vectorTypesToSearch = vectorFields.length ? vectorFields : [null]
|
|
244
|
+
const matchCount = Math.max(offset + limit, 50)
|
|
245
|
+
|
|
246
|
+
for (const vt of vectorTypesToSearch) {
|
|
247
|
+
const { data: matches, error: rpcError } = await adminDb.rpc('match_embeddings', {
|
|
248
|
+
query_embedding: queryEmbedding,
|
|
249
|
+
match_count: matchCount,
|
|
250
|
+
filter_account_ids: accountIds,
|
|
251
|
+
filter_vector_type: vt,
|
|
252
|
+
similarity_threshold: threshold,
|
|
253
|
+
filter_item_types: null,
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
if (rpcError) throw new Error(`rpc_${rpcError.message}`)
|
|
257
|
+
allMatches.push(...(matches || []))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const filtered = allMatches.filter((m: any) => {
|
|
261
|
+
if (m.similarity < threshold) return false
|
|
262
|
+
const meta = m.metadata || {}
|
|
263
|
+
const vectorType = meta.vector_type || ''
|
|
264
|
+
const matchAppId = meta.app_id || null
|
|
265
|
+
|
|
266
|
+
if (vectorFields.length && !allowedVectorTypes.has(vectorType)) return false
|
|
267
|
+
|
|
268
|
+
if (appId && matchAppId !== appId) return false
|
|
269
|
+
|
|
270
|
+
return true
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const bestByDoc = new Map<string, any>()
|
|
274
|
+
for (const m of filtered) {
|
|
275
|
+
const existing = bestByDoc.get(m.document_id)
|
|
276
|
+
if (!existing || m.similarity > existing.similarity) bestByDoc.set(m.document_id, m)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const documentIds = Array.from(bestByDoc.keys()).slice(offset, offset + limit)
|
|
280
|
+
if (documentIds.length === 0) return { results: [], count: 0 }
|
|
281
|
+
|
|
282
|
+
const { data: records, error: recordsError } = await (ctx.db || adminDb)
|
|
283
|
+
.from('items')
|
|
284
|
+
.select('*, types!inner(slug)')
|
|
285
|
+
.in('id', documentIds)
|
|
286
|
+
.in('account_id', accountIds)
|
|
287
|
+
|
|
288
|
+
if (recordsError) throw recordsError
|
|
289
|
+
|
|
290
|
+
const results: any[] = []
|
|
291
|
+
for (const match of documentIds.map((id) => bestByDoc.get(id))) {
|
|
292
|
+
const record = (records || []).find((r: any) => r.id === match.document_id)
|
|
293
|
+
if (!record) continue
|
|
294
|
+
if (record.status !== 'published') continue
|
|
295
|
+
if (!matchesFilters(record, filters)) continue
|
|
296
|
+
|
|
297
|
+
const sanitized = await sanitizeRecordData(ctx, record, 'items')
|
|
298
|
+
if (!sanitized.data || Object.keys(sanitized.data).length === 0) continue
|
|
299
|
+
const recordTypeSlug = (record as any).types?.slug || sanitized.type_slug || record.type_slug
|
|
300
|
+
if (typeSlugs.length > 0 && !allowedTypeSlugs.has(recordTypeSlug || '')) continue
|
|
301
|
+
|
|
302
|
+
const source = match.metadata?.item_type === 'cil_chunk' ? 'code' : 'kb'
|
|
303
|
+
results.push({
|
|
304
|
+
id: record.id,
|
|
305
|
+
type_slug: recordTypeSlug,
|
|
306
|
+
title: sanitized.title || record.title,
|
|
307
|
+
snippet: match.content,
|
|
308
|
+
app_id: record.app_id || match.metadata?.app_id || null,
|
|
309
|
+
similarity: match.similarity,
|
|
310
|
+
status: record.status,
|
|
311
|
+
created_at: record.created_at,
|
|
312
|
+
source,
|
|
313
|
+
data: sanitized.data,
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { results: results.sort((a, b) => b.similarity - a.similarity), count: results.length }
|
|
318
|
+
}
|
|
319
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// CIL[BEGIN chunk_id="llm_module_header_1784700002" type="module_header" identity="module_header" parent="none"]
|
|
2
|
+
// CIL[DOC-MACRO purpose="Broker-backed LLM helpers for core functions"]
|
|
3
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Route OpenAI chat and embedding calls through integration-outbound"]
|
|
4
|
+
// CIL[DOC-CONTEXT module="llm" layer="infrastructure" related_files="[".framework/functions/_shared/agent-runner.ts",".framework/functions/_shared/kb.ts",".framework/functions/embeddings.ts",".framework/functions/integration-outbound.ts",".framework/migrations/051_integration_broker.sql"]"]
|
|
5
|
+
// CIL[DOC-CONTRACT inputs="optional ctx, endpoint, body" outputs="parsed API response" errors="throws on missing key or HTTP failure" side_effects="Calls integration-outbound, writes webhook_deliveries"]
|
|
6
|
+
// CIL[DOC-MICRO logic_summary="Provides callOpenAI, callOpenAIEmbeddings, and callOpenAIChat. Falls back to a system CoreContext when none is provided." notable_decisions="["All OpenAI calls now create an outbound delivery record for audit/retry.","A default system context lets non-handler call sites reuse the helper."]"]
|
|
7
|
+
// CIL[DOC-CONFIDENCE pass="first" score="0.85" flags="none"]
|
|
8
|
+
// CIL[DOC-STABILITY stability="experimental"]
|
|
9
|
+
// CIL[DOC-AUDIENCE audience="developer"]
|
|
10
|
+
// CIL[DOC-VECTOR type="context" content="Layer: infrastructure. Direct callers: 0. Direct calls: 0. Related files: .framework/functions/_shared/agent-runner.ts, .framework/functions/_shared/kb.ts, .framework/functions/embeddings.ts, .framework/functions/integration-outbound.ts, .framework/migrations/051_integration_broker.sql."]
|
|
11
|
+
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
12
|
+
// CIL[DOC-VECTOR type="configuration" content="Requires the system 'openai' integration to be seeded."]
|
|
13
|
+
// CIL[DOC-VECTOR type="constraint" content="Non-200 responses throw."]
|
|
14
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="Replaces direct process.env OPENAI_API_KEY fetches in core."]
|
|
15
|
+
// CIL[DOC-VECTOR type="user_journey" content="AI agents and embedding pipelines use the broker."]
|
|
16
|
+
|
|
17
|
+
import { execute as executeOutbound } from '../integration-outbound'
|
|
18
|
+
import { adminDb } from './db'
|
|
19
|
+
import { SYSTEM_PRINCIPAL } from './principal'
|
|
20
|
+
|
|
21
|
+
export interface LlmContext {
|
|
22
|
+
db?: any
|
|
23
|
+
principal?: any
|
|
24
|
+
accountId?: string | null
|
|
25
|
+
requestId?: string
|
|
26
|
+
integration?: any
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildCoreContext(ctx?: LlmContext): any {
|
|
30
|
+
return ctx || {
|
|
31
|
+
db: adminDb,
|
|
32
|
+
principal: SYSTEM_PRINCIPAL,
|
|
33
|
+
accountId: null,
|
|
34
|
+
requestId: crypto.randomUUID()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function callOpenAI(ctx: LlmContext | undefined, endpoint: string, body: any): Promise<any> {
|
|
39
|
+
const coreCtx = buildCoreContext(ctx)
|
|
40
|
+
const result = await executeOutbound(coreCtx, {
|
|
41
|
+
integration_slug: 'openai',
|
|
42
|
+
action_config: { endpoint, body, credential_field: 'api_token' }
|
|
43
|
+
})
|
|
44
|
+
return result.response_body
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function callOpenAIEmbeddings(
|
|
48
|
+
ctx: LlmContext | undefined,
|
|
49
|
+
inputs: string[],
|
|
50
|
+
model = 'text-embedding-3-small'
|
|
51
|
+
): Promise<any> {
|
|
52
|
+
const integration = ctx?.integration
|
|
53
|
+
const resolvedModel = integration?.config?.models?.embedding || model
|
|
54
|
+
return callOpenAI(ctx, '/embeddings', {
|
|
55
|
+
model: resolvedModel,
|
|
56
|
+
input: inputs
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function callOpenAIChat(
|
|
61
|
+
ctx: LlmContext | undefined,
|
|
62
|
+
messages: any[],
|
|
63
|
+
model = 'gpt-4o-mini',
|
|
64
|
+
options: { temperature?: number; max_tokens?: number; tools?: any[]; tool_choice?: any; response_format?: any } = {}
|
|
65
|
+
): Promise<any> {
|
|
66
|
+
const integration = ctx?.integration
|
|
67
|
+
const resolvedModel = integration?.config?.models?.chat || model
|
|
68
|
+
const body: any = {
|
|
69
|
+
model: resolvedModel,
|
|
70
|
+
messages,
|
|
71
|
+
temperature: options.temperature ?? 0.7,
|
|
72
|
+
max_tokens: options.max_tokens ?? 4000
|
|
73
|
+
}
|
|
74
|
+
if (options.tools?.length) {
|
|
75
|
+
body.tools = options.tools
|
|
76
|
+
body.tool_choice = options.tool_choice ?? 'auto'
|
|
77
|
+
}
|
|
78
|
+
if (options.response_format) {
|
|
79
|
+
body.response_format = options.response_format
|
|
80
|
+
}
|
|
81
|
+
return callOpenAI(ctx, '/chat/completions', body)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function callEmbeddingApi(inputs: string[], model = 'text-embedding-3-small'): Promise<number[][]> {
|
|
85
|
+
if (!inputs.length) throw new Error('No inputs provided')
|
|
86
|
+
const j = await callOpenAIEmbeddings(undefined, inputs, model)
|
|
87
|
+
return (j.data || [])
|
|
88
|
+
.sort((a: any, b: any) => a.index - b.index)
|
|
89
|
+
.map((d: any) => d.embedding)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// CIL[END chunk_id="llm_module_header_1784700002"]
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { CoreContext } from './middleware'
|
|
2
|
+
import { adminDb } from './db'
|
|
3
|
+
import { formatPrincipalForAudit } from './principal'
|
|
4
|
+
|
|
5
|
+
export type LogLevel = 'off' | 'timing' | 'warnings' | 'errors' | 'all'
|
|
6
|
+
|
|
7
|
+
export interface LogContext extends CoreContext {
|
|
8
|
+
appId?: string | null
|
|
9
|
+
source?: string
|
|
10
|
+
logLevel?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AdminDataLogPayload {
|
|
14
|
+
entity: string
|
|
15
|
+
action: string
|
|
16
|
+
method?: string
|
|
17
|
+
durationMs?: number
|
|
18
|
+
recordId?: string
|
|
19
|
+
rowCount?: number
|
|
20
|
+
filters?: Record<string, any>
|
|
21
|
+
typeSlug?: string
|
|
22
|
+
error?: any
|
|
23
|
+
warning?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const VALID_LEVELS: LogLevel[] = ['off', 'timing', 'warnings', 'errors', 'all']
|
|
27
|
+
|
|
28
|
+
function parseLogLevelHeader(value: string | undefined): LogLevel[] {
|
|
29
|
+
if (!value) return []
|
|
30
|
+
const parts = value.split(/[,|]/).map(p => p.trim()).filter(Boolean)
|
|
31
|
+
if (parts.length === 0) {
|
|
32
|
+
const single = value.trim() as LogLevel
|
|
33
|
+
return VALID_LEVELS.includes(single) ? [single] : []
|
|
34
|
+
}
|
|
35
|
+
const levels = parts.filter(p => VALID_LEVELS.includes(p as LogLevel)) as LogLevel[]
|
|
36
|
+
return levels
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getCache(ctx: LogContext): Map<string, any> | undefined {
|
|
40
|
+
return (ctx as any).__loggingCache
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getOrCreateCache(ctx: LogContext): Map<string, any> {
|
|
44
|
+
if (!(ctx as any).__loggingCache) {
|
|
45
|
+
;(ctx as any).__loggingCache = new Map()
|
|
46
|
+
}
|
|
47
|
+
return (ctx as any).__loggingCache
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function findSetting(
|
|
51
|
+
rows: any[],
|
|
52
|
+
scope: string,
|
|
53
|
+
scopeId?: string | null
|
|
54
|
+
): LogLevel[] | undefined {
|
|
55
|
+
const row = rows.find(
|
|
56
|
+
(r) => r.scope === scope && (scopeId === undefined ? r.scope_id == null : r.scope_id === scopeId)
|
|
57
|
+
)
|
|
58
|
+
return row?.value?.levels as LogLevel[] | undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function resolveLogLevels(
|
|
62
|
+
ctx: LogContext,
|
|
63
|
+
options?: { entity?: string; action?: string }
|
|
64
|
+
): Promise<LogLevel[]> {
|
|
65
|
+
const cache = getOrCreateCache(ctx)
|
|
66
|
+
const cacheKey = `levels:${options?.entity || ''}:${options?.action || ''}`
|
|
67
|
+
if (cache.has(cacheKey)) {
|
|
68
|
+
return cache.get(cacheKey)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const defaultLevels: LogLevel[] = ['timing', 'errors']
|
|
72
|
+
|
|
73
|
+
const { data: rows, error } = await adminDb
|
|
74
|
+
.from('system_settings')
|
|
75
|
+
.select('scope, scope_id, value')
|
|
76
|
+
.eq('key', 'logging')
|
|
77
|
+
|
|
78
|
+
if (error || !rows || rows.length === 0) {
|
|
79
|
+
cache.set(cacheKey, defaultLevels)
|
|
80
|
+
return defaultLevels
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const headerLevels = parseLogLevelHeader(ctx.logLevel)
|
|
84
|
+
if (headerLevels.length > 0) {
|
|
85
|
+
cache.set(cacheKey, headerLevels)
|
|
86
|
+
return headerLevels
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let levels: LogLevel[] = findSetting(rows, 'global') || defaultLevels
|
|
90
|
+
|
|
91
|
+
function apply(scope: string, scopeId?: string | null) {
|
|
92
|
+
const found = findSetting(rows, scope, scopeId)
|
|
93
|
+
if (found && found.length > 0) {
|
|
94
|
+
levels = found
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const source = ctx.source || ctx.principal?.provenance?.sourceType
|
|
99
|
+
if (source) apply('source', source)
|
|
100
|
+
|
|
101
|
+
if (ctx.principal?.id) apply('principal', ctx.principal.id)
|
|
102
|
+
|
|
103
|
+
if (ctx.accountId) apply('account', ctx.accountId)
|
|
104
|
+
|
|
105
|
+
if (ctx.appId) apply('app', ctx.appId)
|
|
106
|
+
|
|
107
|
+
if (options?.entity && options?.action) {
|
|
108
|
+
apply('tool', `${options.entity}:${options.action}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (options?.entity) {
|
|
112
|
+
apply('tool', options.entity)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
cache.set(cacheKey, levels)
|
|
116
|
+
return levels
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildMessage(payload: AdminDataLogPayload): string {
|
|
120
|
+
const { entity, action, method, durationMs, recordId, error, warning } = payload
|
|
121
|
+
let msg = `admin-data ${action} ${entity}`
|
|
122
|
+
if (method) msg += ` (${method})`
|
|
123
|
+
if (recordId) msg += ` id:${recordId}`
|
|
124
|
+
if (durationMs !== undefined) msg += ` in ${durationMs}ms`
|
|
125
|
+
if (warning) msg += ` warning: ${warning}`
|
|
126
|
+
if (error) msg += ` failed: ${error.message || String(error)}`
|
|
127
|
+
return msg
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildContext(payload: AdminDataLogPayload): Record<string, any> {
|
|
131
|
+
const ctx: Record<string, any> = {}
|
|
132
|
+
if (payload.durationMs !== undefined) ctx.duration_ms = payload.durationMs
|
|
133
|
+
if (payload.method) ctx.method = payload.method
|
|
134
|
+
if (payload.rowCount !== undefined) ctx.row_count = payload.rowCount
|
|
135
|
+
if (payload.filters && Object.keys(payload.filters).length > 0) ctx.filters = payload.filters
|
|
136
|
+
if (payload.typeSlug) ctx.type_slug = payload.typeSlug
|
|
137
|
+
if (payload.warning) ctx.warning = payload.warning
|
|
138
|
+
if (payload.error) ctx.error_message = payload.error.message || String(payload.error)
|
|
139
|
+
return ctx
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function traceLogFields(ctx: LogContext): Record<string, any> {
|
|
143
|
+
const trace = ctx.trace
|
|
144
|
+
if (!trace) return {}
|
|
145
|
+
return {
|
|
146
|
+
trace_id: trace.traceId,
|
|
147
|
+
span_id: trace.spanId,
|
|
148
|
+
parent_span_id: trace.parentSpanId,
|
|
149
|
+
trace_flags: trace.traceFlags,
|
|
150
|
+
tracestate: trace.traceState,
|
|
151
|
+
sampled: trace.sampled
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildMetadata(
|
|
156
|
+
ctx: LogContext,
|
|
157
|
+
payload: AdminDataLogPayload
|
|
158
|
+
): Record<string, any> {
|
|
159
|
+
const meta: Record<string, any> = {
|
|
160
|
+
request_id: ctx.requestId,
|
|
161
|
+
source: ctx.source || ctx.principal?.provenance?.sourceType,
|
|
162
|
+
entity: payload.entity,
|
|
163
|
+
action: payload.action,
|
|
164
|
+
principal: ctx.principal ? formatPrincipalForAudit(ctx.principal) : null,
|
|
165
|
+
...traceLogFields(ctx)
|
|
166
|
+
}
|
|
167
|
+
if (payload.recordId) meta.record_id = payload.recordId
|
|
168
|
+
if (payload.error && payload.error.stack) {
|
|
169
|
+
meta.error_stack = String(payload.error.stack).split('\n').slice(0, 5).join('\n')
|
|
170
|
+
}
|
|
171
|
+
return meta
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function extractLogResult(result: any): { recordId?: string; rowCount?: number } {
|
|
175
|
+
if (!result || typeof result !== 'object') return {}
|
|
176
|
+
|
|
177
|
+
if (Array.isArray(result)) {
|
|
178
|
+
return { rowCount: result.length }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (typeof result.id === 'string') {
|
|
182
|
+
return { recordId: result.id }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (result.data && Array.isArray(result.data)) {
|
|
186
|
+
const out: any = { rowCount: result.data.length }
|
|
187
|
+
if (result.data.length === 1 && result.data[0]?.id) {
|
|
188
|
+
out.recordId = result.data[0].id
|
|
189
|
+
}
|
|
190
|
+
return out
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (result.data && typeof result.data === 'object' && typeof result.data.id === 'string') {
|
|
194
|
+
return { recordId: result.data.id }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (typeof result.count === 'number') {
|
|
198
|
+
return { rowCount: result.count }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function logAdminDataOperation(
|
|
205
|
+
ctx: LogContext,
|
|
206
|
+
payload: AdminDataLogPayload
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
try {
|
|
209
|
+
if (payload.entity === 'logs' || payload.entity === 'observability') {
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const levels = await resolveLogLevels(ctx, payload)
|
|
214
|
+
|
|
215
|
+
if (levels.includes('off') || levels.length === 0) {
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const isAll = levels.includes('all')
|
|
220
|
+
const isError = !!payload.error
|
|
221
|
+
const isWarning = !!payload.warning
|
|
222
|
+
|
|
223
|
+
if (isError && !isAll && !levels.includes('errors')) return
|
|
224
|
+
if (isWarning && !isAll && !levels.includes('warnings')) return
|
|
225
|
+
if (!isError && !isWarning && !isAll && !levels.includes('timing')) return
|
|
226
|
+
|
|
227
|
+
let level: 'info' | 'warn' | 'error' = 'info'
|
|
228
|
+
if (isError) level = 'error'
|
|
229
|
+
else if (isWarning) level = 'warn'
|
|
230
|
+
|
|
231
|
+
const logRow = {
|
|
232
|
+
level,
|
|
233
|
+
source: 'admin-data',
|
|
234
|
+
source_type: payload.entity,
|
|
235
|
+
source_id: payload.recordId || null,
|
|
236
|
+
person_id: ctx.principal?.id || null,
|
|
237
|
+
created_by: ctx.principal?.id || null,
|
|
238
|
+
app_id: ctx.appId || null,
|
|
239
|
+
account_id: ctx.accountId || null,
|
|
240
|
+
span_name: `admin-data.${payload.action}`,
|
|
241
|
+
kind: 'event',
|
|
242
|
+
started_at: new Date().toISOString(),
|
|
243
|
+
duration_ms: payload.durationMs || null,
|
|
244
|
+
...traceLogFields(ctx),
|
|
245
|
+
message: buildMessage(payload),
|
|
246
|
+
context: buildContext(payload),
|
|
247
|
+
metadata: buildMetadata(ctx, payload),
|
|
248
|
+
created_at: new Date().toISOString()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const logDb = ctx.db || adminDb
|
|
252
|
+
await logDb.from('logs').insert(logRow)
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.error('Failed to write admin-data log:', err)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function withAdminLogging<T>(
|
|
259
|
+
ctx: LogContext,
|
|
260
|
+
params: Omit<AdminDataLogPayload, 'durationMs' | 'error' | 'recordId' | 'rowCount'>,
|
|
261
|
+
operation: () => Promise<T>
|
|
262
|
+
): Promise<T> {
|
|
263
|
+
const start = Date.now()
|
|
264
|
+
let result: T
|
|
265
|
+
let error: any
|
|
266
|
+
let recordId: string | undefined
|
|
267
|
+
let rowCount: number | undefined
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
result = await operation()
|
|
271
|
+
const extracted = extractLogResult(result)
|
|
272
|
+
recordId = extracted.recordId
|
|
273
|
+
rowCount = extracted.rowCount
|
|
274
|
+
return result
|
|
275
|
+
} catch (err) {
|
|
276
|
+
error = err
|
|
277
|
+
throw err
|
|
278
|
+
} finally {
|
|
279
|
+
const durationMs = Date.now() - start
|
|
280
|
+
logAdminDataOperation(ctx, {
|
|
281
|
+
...params,
|
|
282
|
+
durationMs,
|
|
283
|
+
error,
|
|
284
|
+
recordId,
|
|
285
|
+
rowCount
|
|
286
|
+
}).catch(console.error)
|
|
287
|
+
}
|
|
288
|
+
}
|