chore(deps): bump dependencies
This commit is contained in:
@@ -119,9 +119,9 @@ else {
|
||||
const resource = resourceFromAttributes({
|
||||
[ATTR_SERVICE_NAME]: serviceName,
|
||||
[ATTR_SERVICE_VERSION]: env.npm_package_version || '0.0.0',
|
||||
'service.namespace': serviceNamespace,
|
||||
'service.instance.id': instanceId,
|
||||
'deployment.environment': env.NODE_ENV || 'development',
|
||||
'service.instance.id': instanceId,
|
||||
'service.namespace': serviceNamespace,
|
||||
})
|
||||
|
||||
// Traces fan out to independent processors. OTLP (Grafana Tempo) sees every
|
||||
@@ -131,8 +131,8 @@ else {
|
||||
const spanProcessors = []
|
||||
if (otlpEndpoint) {
|
||||
spanProcessors.push(new BatchSpanProcessor(new OTLPTraceExporter({
|
||||
url: `${otlpEndpoint}/v1/traces`,
|
||||
headers,
|
||||
url: `${otlpEndpoint}/v1/traces`,
|
||||
})))
|
||||
}
|
||||
// Langfuse runs on its OWN TracerProvider, isolated from the global NodeSDK
|
||||
@@ -160,12 +160,12 @@ else {
|
||||
resource,
|
||||
sampler: new AlwaysOnSampler(),
|
||||
spanProcessors: [new LangfuseSpanProcessor({
|
||||
publicKey: env.LANGFUSE_PUBLIC_KEY,
|
||||
secretKey: env.LANGFUSE_SECRET_KEY,
|
||||
baseUrl: env.LANGFUSE_BASE_URL,
|
||||
// Railway is long-running → batched. Flushed via
|
||||
// langfuseProvider.shutdown() in the SIGTERM handler below.
|
||||
exportMode: 'batched',
|
||||
publicKey: env.LANGFUSE_PUBLIC_KEY,
|
||||
secretKey: env.LANGFUSE_SECRET_KEY,
|
||||
// Full override of Langfuse's default filter. Export ONLY spans the
|
||||
// @langfuse/tracing SDK created (they carry `langfuse.*` attributes
|
||||
// such as `langfuse.observation.type`). This provider should only ever
|
||||
@@ -196,18 +196,20 @@ else {
|
||||
// pointed at an empty URL.
|
||||
...(otlpEndpoint
|
||||
? {
|
||||
logRecordProcessors: [new BatchLogRecordProcessor({
|
||||
exporter: new OTLPLogExporter({
|
||||
headers,
|
||||
url: `${otlpEndpoint}/v1/logs`,
|
||||
}),
|
||||
})],
|
||||
metricReaders: [new PeriodicExportingMetricReader({
|
||||
exporter: new OTLPMetricExporter({
|
||||
url: `${otlpEndpoint}/v1/metrics`,
|
||||
headers,
|
||||
url: `${otlpEndpoint}/v1/metrics`,
|
||||
}),
|
||||
exportIntervalMillis: 15_000,
|
||||
exportTimeoutMillis: 10_000,
|
||||
})],
|
||||
logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter({
|
||||
url: `${otlpEndpoint}/v1/logs`,
|
||||
headers,
|
||||
}))],
|
||||
}
|
||||
: {}),
|
||||
instrumentations: [
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"dependencies": {
|
||||
"@dotenvx/dotenvx": "catalog:",
|
||||
"@electric-sql/pglite": "catalog:",
|
||||
"@electric-sql/pglite-pgvector": "catalog:",
|
||||
"@guiiai/logg": "catalog:",
|
||||
"@hono/node-server": "catalog:",
|
||||
"@hono/node-ws": "catalog:",
|
||||
|
||||
@@ -45,47 +45,27 @@ function optionalNumberFromString(defaultValue: number, envKey: string, minimum:
|
||||
}
|
||||
|
||||
const EnvSchema = object({
|
||||
HOST: optional(string(), '0.0.0.0'),
|
||||
PORT: optionalIntegerFromString(3000, 'PORT', 1),
|
||||
|
||||
API_SERVER_URL: optional(string(), 'http://localhost:3000'),
|
||||
AUTH_SERVER_URL: optional(string(), 'http://localhost:3000'),
|
||||
AUTH_SERVER_INTERNAL_URL: optional(string()),
|
||||
|
||||
// Canonical user-facing web app origin. Used as the Stripe redirect base
|
||||
// (success_url / cancel_url / portal return_url) when a request has no trusted
|
||||
// browser origin — notably the Electron desktop renderer, which loads from
|
||||
// file:// and sends no usable web origin. Web/mobile requests keep returning to
|
||||
// their own origin; only origin-less clients fall back to this.
|
||||
WEB_APP_URL: optional(string(), 'https://airi.moeru.ai'),
|
||||
|
||||
// Comma-separated exact origins (e.g. Capacitor dev server `https://10.x:5273`).
|
||||
// Prefer this over broad private-IP regex heuristics in production-like configs.
|
||||
ADDITIONAL_TRUSTED_ORIGINS: optional(
|
||||
AdditionalTrustedOriginsSchema,
|
||||
'',
|
||||
),
|
||||
API_SERVER_URL: optional(string(), 'http://localhost:3000'),
|
||||
|
||||
AUTH_SERVER_INTERNAL_URL: optional(string()),
|
||||
AUTH_SERVER_URL: optional(string(), 'http://localhost:3000'),
|
||||
DATABASE_URL: pipe(string(), nonEmpty('DATABASE_URL is required')),
|
||||
REDIS_URL: pipe(string(), nonEmpty('REDIS_URL is required')),
|
||||
|
||||
// Testing-only bearer token bypass. Keep unset in production. When set,
|
||||
// Authorization: Bearer $TEST_AUTH_TOKEN resolves to the virtual user below
|
||||
// through resolveRequestAuth without creating an Auth session row.
|
||||
TEST_AUTH_TOKEN: optional(string(), ''),
|
||||
TEST_AUTH_USER_ID: optional(pipe(string(), nonEmpty('TEST_AUTH_USER_ID must not be empty when set')), 'test-user'),
|
||||
TEST_AUTH_USER_EMAIL: optional(pipe(string(), nonEmpty('TEST_AUTH_USER_EMAIL must not be empty when set')), 'test@example.com'),
|
||||
TEST_AUTH_USER_NAME: optional(pipe(string(), nonEmpty('TEST_AUTH_USER_NAME must not be empty when set')), 'Test User'),
|
||||
DB_POOL_CONNECTION_TIMEOUT_MS: optionalIntegerFromString(5000, 'DB_POOL_CONNECTION_TIMEOUT_MS', 1),
|
||||
|
||||
STRIPE_SECRET_KEY: optional(string()),
|
||||
STRIPE_WEBHOOK_SECRET: optional(string()),
|
||||
DB_POOL_IDLE_TIMEOUT_MS: optionalIntegerFromString(30000, 'DB_POOL_IDLE_TIMEOUT_MS', 1),
|
||||
|
||||
// LLM/TTS gateway is fully internalised by the in-process router; provider
|
||||
// baseURLs live per-upstream inside LLM_ROUTER_CONFIG, and the default chat /
|
||||
// tts model aliases moved to configKV (DEFAULT_CHAT_MODEL / DEFAULT_TTS_MODEL)
|
||||
// so they're hot-swappable via Pub/Sub invalidation. No env entries needed
|
||||
// here.
|
||||
DB_POOL_KEEPALIVE_INITIAL_DELAY_MS: optionalIntegerFromString(10000, 'DB_POOL_KEEPALIVE_INITIAL_DELAY_MS', 1),
|
||||
// Database pool
|
||||
DB_POOL_MAX: optionalIntegerFromString(20, 'DB_POOL_MAX', 1),
|
||||
|
||||
HOST: optional(string(), '0.0.0.0'),
|
||||
// Envelope-encryption master key for in-process LLM/TTS router (KTD-5).
|
||||
// Stored as base64-encoded 32 random bytes. Validator decodes + asserts the
|
||||
// 32-byte length at parse time so a misconfigured key fails the deploy
|
||||
@@ -107,27 +87,47 @@ const EnvSchema = object({
|
||||
transform(b64 => Buffer.from(b64, 'base64')),
|
||||
check(buf => buf.length === 32, 'LLM_ROUTER_MASTER_KEY_PREVIOUS must decode to exactly 32 bytes when set'),
|
||||
)),
|
||||
OTEL_DEBUG: optional(string()),
|
||||
|
||||
// Database pool
|
||||
DB_POOL_MAX: optionalIntegerFromString(20, 'DB_POOL_MAX', 1),
|
||||
DB_POOL_IDLE_TIMEOUT_MS: optionalIntegerFromString(30000, 'DB_POOL_IDLE_TIMEOUT_MS', 1),
|
||||
DB_POOL_CONNECTION_TIMEOUT_MS: optionalIntegerFromString(5000, 'DB_POOL_CONNECTION_TIMEOUT_MS', 1),
|
||||
DB_POOL_KEEPALIVE_INITIAL_DELAY_MS: optionalIntegerFromString(10000, 'DB_POOL_KEEPALIVE_INITIAL_DELAY_MS', 1),
|
||||
|
||||
// PostHog product-event forwarding for server-confirmed funnel facts.
|
||||
// Defaults to the shared AIRI project key (same browser-safe phc_* key the
|
||||
// client surfaces embed in posthog.config.ts), so forwarding is on out of
|
||||
// the box. Set to an empty string to disable server-side product analytics.
|
||||
POSTHOG_PROJECT_KEY: optional(string(), 'phc_pzjziJjrVZpa9SqnQqq0QEKvkmuCPH7GDTA6TbRTEf9'), // cspell:disable-line
|
||||
POSTHOG_API_HOST: optional(string(), 'https://t.airi.build'),
|
||||
|
||||
// OpenTelemetry
|
||||
OTEL_SERVICE_NAMESPACE: optional(string(), 'airi'),
|
||||
OTEL_SERVICE_NAME: optional(string(), 'server'),
|
||||
OTEL_TRACES_SAMPLING_RATIO: optionalNumberFromString(1, 'OTEL_TRACES_SAMPLING_RATIO', 0, 1),
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: optional(string()),
|
||||
OTEL_EXPORTER_OTLP_HEADERS: optional(string()),
|
||||
OTEL_DEBUG: optional(string()),
|
||||
|
||||
// LLM/TTS gateway is fully internalised by the in-process router; provider
|
||||
// baseURLs live per-upstream inside LLM_ROUTER_CONFIG, and the default chat /
|
||||
// tts model aliases moved to configKV (DEFAULT_CHAT_MODEL / DEFAULT_TTS_MODEL)
|
||||
// so they're hot-swappable via Pub/Sub invalidation. No env entries needed
|
||||
// here.
|
||||
|
||||
OTEL_SERVICE_NAME: optional(string(), 'server'),
|
||||
// OpenTelemetry
|
||||
OTEL_SERVICE_NAMESPACE: optional(string(), 'airi'),
|
||||
|
||||
OTEL_TRACES_SAMPLING_RATIO: optionalNumberFromString(1, 'OTEL_TRACES_SAMPLING_RATIO', 0, 1),
|
||||
PORT: optionalIntegerFromString(3000, 'PORT', 1),
|
||||
POSTHOG_API_HOST: optional(string(), 'https://t.airi.build'),
|
||||
// PostHog product-event forwarding for server-confirmed funnel facts.
|
||||
// Defaults to the shared AIRI project key (same browser-safe phc_* key the
|
||||
// client surfaces embed in stage-shared/analytics/posthog), so forwarding is on out of
|
||||
// the box. Set to an empty string to disable server-side product analytics.
|
||||
POSTHOG_PROJECT_KEY: optional(string(), 'phc_pzjziJjrVZpa9SqnQqq0QEKvkmuCPH7GDTA6TbRTEf9'), // cspell:disable-line
|
||||
|
||||
REDIS_URL: pipe(string(), nonEmpty('REDIS_URL is required')),
|
||||
STRIPE_SECRET_KEY: optional(string()),
|
||||
|
||||
STRIPE_WEBHOOK_SECRET: optional(string()),
|
||||
// Testing-only bearer token bypass. Keep unset in production. When set,
|
||||
// Authorization: Bearer $TEST_AUTH_TOKEN resolves to the virtual user below
|
||||
// through resolveRequestAuth without creating an Auth session row.
|
||||
TEST_AUTH_TOKEN: optional(string(), ''),
|
||||
TEST_AUTH_USER_EMAIL: optional(pipe(string(), nonEmpty('TEST_AUTH_USER_EMAIL must not be empty when set')), 'test@example.com'),
|
||||
TEST_AUTH_USER_ID: optional(pipe(string(), nonEmpty('TEST_AUTH_USER_ID must not be empty when set')), 'test-user'),
|
||||
TEST_AUTH_USER_NAME: optional(pipe(string(), nonEmpty('TEST_AUTH_USER_NAME must not be empty when set')), 'Test User'),
|
||||
// Canonical user-facing web app origin. Used as the Stripe redirect base
|
||||
// (success_url / cancel_url / portal return_url) when a request has no trusted
|
||||
// browser origin — notably the Electron desktop renderer, which loads from
|
||||
// file:// and sends no usable web origin. Web/mobile requests keep returning to
|
||||
// their own origin; only origin-less clients fall back to this.
|
||||
WEB_APP_URL: optional(string(), 'https://airi.moeru.ai'),
|
||||
})
|
||||
|
||||
export type Env = InferOutput<typeof EnvSchema>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { mockDB } from './mock-db'
|
||||
|
||||
describe('mockDB', () => {
|
||||
it('registers the pgvector extension', async () => {
|
||||
const db = await mockDB({})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// PGlite 0.5 moved pgvector from the main package to a separate package.
|
||||
// The old subpath import prevents this test database from loading.
|
||||
//
|
||||
// The helper now registers the separate extension before it creates the schema.
|
||||
const result = await db.execute<{ distance: number }>(sql`
|
||||
SELECT '[1, 2, 3]'::vector <-> '[1, 2, 4]'::vector AS distance
|
||||
`)
|
||||
|
||||
expect(result.rows).toEqual([{ distance: 1 }])
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Database } from './db'
|
||||
|
||||
import { PGlite } from '@electric-sql/pglite'
|
||||
import { vector } from '@electric-sql/pglite/vector'
|
||||
import { vector } from '@electric-sql/pglite-pgvector'
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { drizzle } from 'drizzle-orm/pglite'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
@@ -51,27 +51,12 @@ else {
|
||||
const resource = resourceFromAttributes({
|
||||
[ATTR_SERVICE_NAME]: env.OTEL_SERVICE_NAME || 'auth-server',
|
||||
[ATTR_SERVICE_VERSION]: env.npm_package_version || '0.0.0',
|
||||
'service.namespace': env.OTEL_SERVICE_NAMESPACE || 'airi',
|
||||
'service.instance.id': instanceId,
|
||||
'deployment.environment': env.NODE_ENV || 'development',
|
||||
'service.instance.id': instanceId,
|
||||
'service.namespace': env.OTEL_SERVICE_NAMESPACE || 'airi',
|
||||
})
|
||||
|
||||
const sdk = new NodeSDK({
|
||||
resource,
|
||||
sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(samplingRatio) }),
|
||||
spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({
|
||||
url: `${otlpEndpoint}/v1/traces`,
|
||||
headers,
|
||||
}))],
|
||||
metricReaders: [new PeriodicExportingMetricReader({
|
||||
exporter: new OTLPMetricExporter({ url: `${otlpEndpoint}/v1/metrics`, headers }),
|
||||
exportIntervalMillis: 15_000,
|
||||
exportTimeoutMillis: 10_000,
|
||||
})],
|
||||
logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter({
|
||||
url: `${otlpEndpoint}/v1/logs`,
|
||||
headers,
|
||||
}))],
|
||||
instrumentations: [
|
||||
new HttpInstrumentation({ disableIncomingRequestInstrumentation: true }),
|
||||
new PgInstrumentation({ enhancedDatabaseReporting: true }),
|
||||
@@ -79,6 +64,23 @@ else {
|
||||
new RuntimeNodeInstrumentation(),
|
||||
new UndiciInstrumentation(),
|
||||
],
|
||||
logRecordProcessors: [new BatchLogRecordProcessor({
|
||||
exporter: new OTLPLogExporter({
|
||||
headers,
|
||||
url: `${otlpEndpoint}/v1/logs`,
|
||||
}),
|
||||
})],
|
||||
metricReaders: [new PeriodicExportingMetricReader({
|
||||
exporter: new OTLPMetricExporter({ headers, url: `${otlpEndpoint}/v1/metrics` }),
|
||||
exportIntervalMillis: 15_000,
|
||||
exportTimeoutMillis: 10_000,
|
||||
})],
|
||||
resource,
|
||||
sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(samplingRatio) }),
|
||||
spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({
|
||||
headers,
|
||||
url: `${otlpEndpoint}/v1/traces`,
|
||||
}))],
|
||||
})
|
||||
|
||||
sdk.start()
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"@electric-sql/pglite": "catalog:",
|
||||
"@types/pg": "catalog:",
|
||||
"drizzle-kit": "catalog:",
|
||||
"kysely": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user