Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23b830074a | ||
|
|
fa5908a749 | ||
|
|
26045d089c | ||
|
|
edfb15c010 | ||
|
|
1f76ae650f | ||
|
|
36dc608b58 | ||
|
|
bc0735729f | ||
|
|
88e553734e | ||
|
|
bd5495321e | ||
|
|
e8ebfb8e3e | ||
|
|
8c076b6b50 | ||
|
|
2cdc3cab7f | ||
|
|
729c2132af | ||
|
|
d3627bf225 |
@@ -0,0 +1,119 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public/integrations/blob-storage
|
||||
endpoints:
|
||||
getBlobStorageIntegrations:
|
||||
docs: Get all blob storage integrations for the organization (requires organization-scoped API key)
|
||||
method: GET
|
||||
path: ""
|
||||
response: BlobStorageIntegrationsResponse
|
||||
|
||||
upsertBlobStorageIntegration:
|
||||
docs: Create or update a blob storage integration for a specific project (requires organization-scoped API key). The configuration is validated by performing a test upload to the bucket.
|
||||
method: PUT
|
||||
path: ""
|
||||
request: CreateBlobStorageIntegrationRequest
|
||||
response: BlobStorageIntegrationResponse
|
||||
|
||||
deleteBlobStorageIntegration:
|
||||
docs: Delete a blob storage integration by ID (requires organization-scoped API key)
|
||||
method: DELETE
|
||||
path: "/{id}"
|
||||
path-parameters:
|
||||
id: string
|
||||
response: BlobStorageIntegrationDeletionResponse
|
||||
|
||||
types:
|
||||
BlobStorageIntegrationType:
|
||||
enum:
|
||||
- S3
|
||||
- S3_COMPATIBLE
|
||||
- AZURE_BLOB_STORAGE
|
||||
|
||||
BlobStorageIntegrationFileType:
|
||||
enum:
|
||||
- JSON
|
||||
- CSV
|
||||
- JSONL
|
||||
|
||||
BlobStorageExportMode:
|
||||
enum:
|
||||
- FULL_HISTORY
|
||||
- FROM_TODAY
|
||||
- FROM_CUSTOM_DATE
|
||||
|
||||
BlobStorageExportFrequency:
|
||||
enum:
|
||||
- hourly
|
||||
- daily
|
||||
- weekly
|
||||
|
||||
CreateBlobStorageIntegrationRequest:
|
||||
properties:
|
||||
projectId:
|
||||
type: string
|
||||
docs: ID of the project in which to configure the blob storage integration
|
||||
type: BlobStorageIntegrationType
|
||||
bucketName:
|
||||
type: string
|
||||
docs: Name of the storage bucket
|
||||
endpoint:
|
||||
type: optional<string>
|
||||
docs: Custom endpoint URL (required for S3_COMPATIBLE type)
|
||||
region:
|
||||
type: string
|
||||
docs: Storage region
|
||||
accessKeyId:
|
||||
type: optional<string>
|
||||
docs: Access key ID for authentication
|
||||
secretAccessKey:
|
||||
type: optional<string>
|
||||
docs: Secret access key for authentication (will be encrypted when stored)
|
||||
prefix:
|
||||
type: optional<string>
|
||||
docs: Path prefix for exported files (must end with forward slash if provided)
|
||||
exportFrequency: BlobStorageExportFrequency
|
||||
enabled:
|
||||
type: boolean
|
||||
docs: Whether the integration is active
|
||||
forcePathStyle:
|
||||
type: boolean
|
||||
docs: Use path-style URLs for S3 requests
|
||||
fileType: BlobStorageIntegrationFileType
|
||||
exportMode: BlobStorageExportMode
|
||||
exportStartDate:
|
||||
type: optional<datetime>
|
||||
docs: Custom start date for exports (required when exportMode is FROM_CUSTOM_DATE)
|
||||
|
||||
BlobStorageIntegrationResponse:
|
||||
properties:
|
||||
id: string
|
||||
projectId: string
|
||||
type: BlobStorageIntegrationType
|
||||
bucketName: string
|
||||
endpoint: optional<string>
|
||||
region: string
|
||||
accessKeyId: optional<string>
|
||||
prefix: string
|
||||
exportFrequency: BlobStorageExportFrequency
|
||||
enabled: boolean
|
||||
forcePathStyle: boolean
|
||||
fileType: BlobStorageIntegrationFileType
|
||||
exportMode: BlobStorageExportMode
|
||||
exportStartDate: optional<datetime>
|
||||
nextSyncAt: optional<datetime>
|
||||
lastSyncAt: optional<datetime>
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
|
||||
BlobStorageIntegrationsResponse:
|
||||
properties:
|
||||
data: list<BlobStorageIntegrationResponse>
|
||||
|
||||
BlobStorageIntegrationDeletionResponse:
|
||||
properties:
|
||||
message: string
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "3.109.0",
|
||||
"version": "3.110.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -113,7 +113,6 @@
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"kysely-codegen": "^0.16.8",
|
||||
"nodemon": "^3.1.7",
|
||||
"prettier": "^3.6.2",
|
||||
"prisma": "^6.10.1",
|
||||
"prisma-erd-generator": "^1.11.2",
|
||||
|
||||
@@ -110,6 +110,9 @@ async function main() {
|
||||
},
|
||||
});
|
||||
|
||||
// Realistic support chat scenario
|
||||
await createSupportChatSession(project1);
|
||||
|
||||
await prisma.organizationMembership.upsert({
|
||||
where: {
|
||||
orgId_userId: {
|
||||
@@ -771,6 +774,24 @@ async function createTraceSessions(project1: Project, project2: Project) {
|
||||
}
|
||||
}
|
||||
|
||||
async function createSupportChatSession(project: Project) {
|
||||
const sessionId = "support-chat-session";
|
||||
await prisma.traceSession.upsert({
|
||||
where: {
|
||||
id_projectId: {
|
||||
id: sessionId,
|
||||
projectId: project.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
id: sessionId,
|
||||
projectId: project.id,
|
||||
environment: "default",
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
}
|
||||
|
||||
async function generateConfigs(project: Project) {
|
||||
const configNameAndId: {
|
||||
name: string;
|
||||
|
||||
@@ -952,6 +952,310 @@ export class DataGenerator {
|
||||
: JSON.stringify(this.fileContent.chatMlJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates realistic support chat session data with conversational flow.
|
||||
* Use for: Demonstrating session-based conversations with tool calls and scoring.
|
||||
*/
|
||||
generateSupportChatSessionData(projectId: string): {
|
||||
traces: TraceRecordInsertType[];
|
||||
observations: ObservationRecordInsertType[];
|
||||
scores: ScoreRecordInsertType[];
|
||||
} {
|
||||
// Domain: Art museum membership support (non-tech)
|
||||
const dialogues = [
|
||||
{
|
||||
user: "Hi—my family membership at the City Art Museum was charged twice this month.",
|
||||
assistant:
|
||||
"Sorry about that. I can help. Could you share your membership ID and the last 4 digits of the card?",
|
||||
},
|
||||
{
|
||||
user: "Membership ID is CAM-48291, card ending 4417.",
|
||||
assistant: "Thanks. I'll check your billing records now.",
|
||||
tool: {
|
||||
name: "billing.lookup",
|
||||
input: { membershipId: "CAM-48291", last4: "4417" },
|
||||
output: {
|
||||
duplicateCharge: true,
|
||||
amounts: [95.0, 95.0],
|
||||
date: "2025-08-28",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
user: "Okay.",
|
||||
assistant:
|
||||
"I found a duplicate charge on 2025-08-28 for $95. I can issue a refund to the original payment method. Shall I proceed?",
|
||||
},
|
||||
{
|
||||
user: "Yes, please issue the refund.",
|
||||
assistant: "Processing your refund now. One moment.",
|
||||
tool: {
|
||||
name: "billing.refund",
|
||||
input: { membershipId: "CAM-48291", amount: 95.0 },
|
||||
output: { status: "success", refundId: "RFND-20931" },
|
||||
},
|
||||
},
|
||||
{
|
||||
user: "Thank you!",
|
||||
assistant:
|
||||
"Refund RFND-20931 has been issued. You'll see it on your statement within 3–5 business days.",
|
||||
},
|
||||
{
|
||||
user: "No, that's all. Appreciate the quick help!",
|
||||
assistant: "Happy to help. Enjoy your next visit to the museum!",
|
||||
},
|
||||
// a couple more lightweight turns for scrolling realism
|
||||
{
|
||||
user: "Oh, and do members get early access to exhibitions?",
|
||||
assistant:
|
||||
"Yes—members get a 48-hour early booking window and a preview evening invite.",
|
||||
},
|
||||
{
|
||||
user: "Perfect.",
|
||||
assistant: "You're all set. Have a great day!",
|
||||
},
|
||||
];
|
||||
|
||||
const now = Date.now();
|
||||
const traces: TraceRecordInsertType[] = dialogues.map((d, index) => ({
|
||||
id: `support-chat-${index}-${projectId.slice(-8)}`,
|
||||
timestamp: now + index * 1000,
|
||||
name: "SupportChatSession",
|
||||
user_id: null,
|
||||
metadata: { scenario: "support-chat" },
|
||||
release: null,
|
||||
version: null,
|
||||
project_id: projectId,
|
||||
environment: "default",
|
||||
public: false,
|
||||
bookmarked: false,
|
||||
tags: ["support", "chat", "session"],
|
||||
input: JSON.stringify(
|
||||
d.tool
|
||||
? {
|
||||
messages: [
|
||||
{ role: "user", content: d.user },
|
||||
{ role: "assistant", content: d.assistant },
|
||||
{
|
||||
role: "tool",
|
||||
name: d.tool.name,
|
||||
content: d.tool.output,
|
||||
},
|
||||
],
|
||||
}
|
||||
: { messages: [{ role: "user", content: d.user }] },
|
||||
),
|
||||
output: JSON.stringify({ role: "assistant", content: d.assistant }),
|
||||
session_id: "support-chat-session",
|
||||
created_at: now + index * 1000,
|
||||
updated_at: now + index * 1000 + 500,
|
||||
event_ts: now + index * 1000,
|
||||
is_deleted: 0,
|
||||
}));
|
||||
|
||||
// Create one GENERATION observation per trace
|
||||
const observations: ObservationRecordInsertType[] = dialogues
|
||||
.map((d, index) => {
|
||||
const start = now + index * 1000 + 50;
|
||||
const end = start + 400 + Math.floor(Math.random() * 400);
|
||||
const inputTokens = 80 + Math.floor(Math.random() * 60);
|
||||
const outputTokens = 60 + Math.floor(Math.random() * 60);
|
||||
const totalTokens = inputTokens + outputTokens;
|
||||
|
||||
const baseGen: ObservationRecordInsertType = {
|
||||
id: `support-chat-${index}-${projectId.slice(-8)}-gen`,
|
||||
trace_id: `support-chat-${index}-${projectId.slice(-8)}`,
|
||||
project_id: projectId,
|
||||
type: "GENERATION",
|
||||
parent_observation_id: null,
|
||||
environment: "default",
|
||||
start_time: start,
|
||||
end_time: end,
|
||||
name: "llm-generation",
|
||||
metadata: {},
|
||||
level: "DEFAULT",
|
||||
status_message: null,
|
||||
version: null,
|
||||
input: JSON.stringify({
|
||||
messages: [
|
||||
{ role: "user", content: d.user },
|
||||
d.tool
|
||||
? {
|
||||
role: "tool",
|
||||
name: d.tool.name,
|
||||
content: d.tool.output,
|
||||
}
|
||||
: undefined,
|
||||
].filter(Boolean),
|
||||
}),
|
||||
output: JSON.stringify({ role: "assistant", content: d.assistant }),
|
||||
provided_model_name: "gpt-4o",
|
||||
internal_model_id: null,
|
||||
model_parameters: JSON.stringify({ temperature: 0.2 }),
|
||||
provided_usage_details: {
|
||||
input: inputTokens,
|
||||
output: outputTokens,
|
||||
total: totalTokens,
|
||||
},
|
||||
usage_details: {
|
||||
input: inputTokens,
|
||||
output: outputTokens,
|
||||
total: totalTokens,
|
||||
},
|
||||
provided_cost_details: {
|
||||
input: Math.round(inputTokens * 2) / 1_000_000,
|
||||
output: Math.round(outputTokens * 3) / 1_000_000,
|
||||
total: Math.round(totalTokens * 5) / 1_000_000,
|
||||
},
|
||||
cost_details: {
|
||||
input: Math.round(inputTokens * 2) / 1_000_000,
|
||||
output: Math.round(outputTokens * 3) / 1_000_000,
|
||||
total: Math.round(totalTokens * 5) / 1_000_000,
|
||||
},
|
||||
total_cost: Math.round(totalTokens * 5) / 1_000_000,
|
||||
completion_start_time: start + 120,
|
||||
prompt_id: null,
|
||||
prompt_name: null,
|
||||
prompt_version: null,
|
||||
created_at: start,
|
||||
updated_at: end,
|
||||
event_ts: start,
|
||||
is_deleted: 0,
|
||||
};
|
||||
|
||||
if (!d.tool) return [baseGen];
|
||||
|
||||
const toolObs: ObservationRecordInsertType = {
|
||||
id: `support-chat-${index}-${projectId.slice(-8)}-tool`,
|
||||
trace_id: `support-chat-${index}-${projectId.slice(-8)}`,
|
||||
project_id: projectId,
|
||||
type: "TOOL",
|
||||
parent_observation_id: null,
|
||||
environment: "default",
|
||||
start_time: start - 40,
|
||||
end_time: start - 5,
|
||||
name: d.tool.name,
|
||||
metadata: {},
|
||||
level: "DEFAULT",
|
||||
status_message: null,
|
||||
version: null,
|
||||
input: JSON.stringify(d.tool.input),
|
||||
output: JSON.stringify(d.tool.output),
|
||||
provided_model_name: null,
|
||||
internal_model_id: null,
|
||||
model_parameters: null,
|
||||
provided_usage_details: {},
|
||||
usage_details: {},
|
||||
provided_cost_details: {},
|
||||
cost_details: {},
|
||||
total_cost: null,
|
||||
completion_start_time: null,
|
||||
prompt_id: null,
|
||||
prompt_name: null,
|
||||
prompt_version: null,
|
||||
created_at: start - 40,
|
||||
updated_at: start - 5,
|
||||
event_ts: start - 40,
|
||||
is_deleted: 0,
|
||||
};
|
||||
|
||||
return [toolObs, baseGen];
|
||||
})
|
||||
.flat();
|
||||
|
||||
// Create a couple of scores per trace
|
||||
const scores: ScoreRecordInsertType[] = dialogues
|
||||
.map((_, index) => {
|
||||
const baseTs = now + index * 1000 + 600;
|
||||
const helpfulness: ScoreRecordInsertType = {
|
||||
id: `support-chat-${index}-${projectId.slice(-8)}-score-helpfulness`,
|
||||
project_id: projectId,
|
||||
trace_id: `support-chat-${index}-${projectId.slice(-8)}`,
|
||||
session_id: null,
|
||||
dataset_run_id: null,
|
||||
observation_id: null,
|
||||
environment: "default",
|
||||
name: "helpfulness",
|
||||
value: 70 + Math.random() * 25,
|
||||
source: "API",
|
||||
comment: "Heuristic helpfulness score",
|
||||
metadata: {},
|
||||
author_user_id: null,
|
||||
config_id: null,
|
||||
data_type: "NUMERIC",
|
||||
string_value: null,
|
||||
queue_id: null,
|
||||
created_at: baseTs,
|
||||
updated_at: baseTs,
|
||||
timestamp: baseTs,
|
||||
event_ts: baseTs,
|
||||
is_deleted: 0,
|
||||
};
|
||||
|
||||
const safeVal = Math.random() > 0.1 ? 1 : 0;
|
||||
const safety: ScoreRecordInsertType = {
|
||||
id: `support-chat-${index}-${projectId.slice(-8)}-score-safety`,
|
||||
project_id: projectId,
|
||||
trace_id: `support-chat-${index}-${projectId.slice(-8)}`,
|
||||
session_id: null,
|
||||
dataset_run_id: null,
|
||||
observation_id: null,
|
||||
environment: "default",
|
||||
name: "safe",
|
||||
value: safeVal,
|
||||
source: "API",
|
||||
comment: "Content safety",
|
||||
metadata: {},
|
||||
author_user_id: null,
|
||||
config_id: null,
|
||||
data_type: "BOOLEAN",
|
||||
string_value: safeVal === 1 ? "true" : "false",
|
||||
queue_id: null,
|
||||
created_at: baseTs + 10,
|
||||
updated_at: baseTs + 10,
|
||||
timestamp: baseTs + 10,
|
||||
event_ts: baseTs + 10,
|
||||
is_deleted: 0,
|
||||
};
|
||||
|
||||
// Optional: resolution score on last turn
|
||||
const isFinal = index === dialogues.length - 1;
|
||||
const resolved: ScoreRecordInsertType | null = isFinal
|
||||
? {
|
||||
id: `support-chat-${index}-${projectId.slice(-8)}-score-resolved`,
|
||||
project_id: projectId,
|
||||
trace_id: `support-chat-${index}-${projectId.slice(-8)}`,
|
||||
session_id: null,
|
||||
dataset_run_id: null,
|
||||
observation_id: null,
|
||||
environment: "default",
|
||||
name: "resolved",
|
||||
value: 1,
|
||||
source: "API",
|
||||
comment: "Conversation resolved",
|
||||
metadata: {},
|
||||
author_user_id: null,
|
||||
config_id: null,
|
||||
data_type: "BOOLEAN",
|
||||
string_value: "true",
|
||||
queue_id: null,
|
||||
created_at: baseTs + 20,
|
||||
updated_at: baseTs + 20,
|
||||
timestamp: baseTs + 20,
|
||||
event_ts: baseTs + 20,
|
||||
is_deleted: 0,
|
||||
}
|
||||
: null;
|
||||
|
||||
return [helpfulness, safety, resolved].filter(
|
||||
Boolean,
|
||||
) as ScoreRecordInsertType[];
|
||||
})
|
||||
.flat();
|
||||
|
||||
return { traces, observations, scores };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates exactly one score per evaluation trace with prefixed IDs.
|
||||
* Use for: Evaluation traces that need score validation, evaluator testing.
|
||||
|
||||
@@ -321,6 +321,9 @@ export class SeederOrchestrator {
|
||||
// Create synthetic data
|
||||
await this.createSyntheticData(projectIds, opts);
|
||||
|
||||
// Create traces for a realistic chat session
|
||||
await this.createSupportChatSessionTraces(projectIds);
|
||||
|
||||
// Log completion statistics (commented out to reduce terminal noise)
|
||||
await this.logStatistics();
|
||||
|
||||
@@ -378,4 +381,27 @@ export class SeederOrchestrator {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createSupportChatSessionTraces(projectIds: string[]): Promise<void> {
|
||||
logger.info(
|
||||
`Creating support chat session data for ${projectIds.length} projects.`,
|
||||
);
|
||||
|
||||
for (const projectId of projectIds) {
|
||||
logger.info(`Processing support chat session for project ${projectId}`);
|
||||
|
||||
// Generate data using the data generator
|
||||
const { traces, observations, scores } =
|
||||
this.dataGenerator.generateSupportChatSessionData(projectId);
|
||||
|
||||
try {
|
||||
await this.queryBuilder.executeTracesInsert(traces);
|
||||
await this.queryBuilder.executeObservationsInsert(observations);
|
||||
await this.queryBuilder.executeScoresInsert(scores);
|
||||
} catch (error) {
|
||||
logger.error(`✗ Support chat session insert failed:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,6 +512,7 @@ export class OtelIngestionProcessor {
|
||||
metadata: {
|
||||
...resourceAttributeMetadata,
|
||||
...this.extractMetadata(attributes, "trace"),
|
||||
...this.extractMetadata(attributes, "observation"),
|
||||
...(isLangfuseSDKSpans
|
||||
? {}
|
||||
: { attributes: spanAttributesInMetadata }),
|
||||
@@ -1079,6 +1080,13 @@ export class OtelIngestionProcessor {
|
||||
return { input, output };
|
||||
}
|
||||
|
||||
// GCP Vertex Agent Tool call input and output
|
||||
input = attributes["gcp.vertex.agent.tool_call_args"];
|
||||
output = attributes["gcp.vertex.agent.tool_response"];
|
||||
if (input || output) {
|
||||
return { input, output };
|
||||
}
|
||||
|
||||
// TraceLoop uses attributes property
|
||||
const inputAttributes = Object.keys(attributes).filter((key) =>
|
||||
key.startsWith("gen_ai.prompt"),
|
||||
|
||||
@@ -57,6 +57,7 @@ export const getTimeframesTracesAMT = (
|
||||
): TracesAMTs => {
|
||||
if (!fromTimestamp) {
|
||||
// The TracesAllAMT must always be returned if there is no timestamp.
|
||||
console.log("No timestamp provided, returning TracesAllAMT");
|
||||
return TracesAMTs.TracesAllAMT;
|
||||
}
|
||||
|
||||
@@ -89,6 +90,8 @@ export const getTimeframesTracesAMT = (
|
||||
* @param {string} traceId - ID of the trace to check
|
||||
* @param {Date} timestamp - Timestamp for time-based filtering, uses event payload or job timestamp
|
||||
* @param {FilterState} filter - Filter for the trace
|
||||
* @param {Date} maxTimeStamp - Upper bound on timestamp
|
||||
* @param {Date} exactTimestamp - Exact match for the trace
|
||||
* @returns {Promise<boolean>} - True if trace exists
|
||||
*
|
||||
* Notes:
|
||||
|
||||
Generated
+30
-354
@@ -58,7 +58,7 @@ importers:
|
||||
version: 15.5.2(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
next-auth:
|
||||
specifier: ^4.24.11
|
||||
version: 4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@15.5.2(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.9.15)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
version: 4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@15.5.2(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.9.15)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
zod:
|
||||
specifier: ^3.25.62
|
||||
version: 3.25.62
|
||||
@@ -238,7 +238,7 @@ importers:
|
||||
version: 4.1.1
|
||||
next-auth:
|
||||
specifier: ^4.24.11
|
||||
version: 4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@15.5.2(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.9.15)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
version: 4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@15.5.2(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.9.15)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
nodemailer:
|
||||
specifier: ^6.9.15
|
||||
version: 6.9.15
|
||||
@@ -303,9 +303,6 @@ importers:
|
||||
kysely-codegen:
|
||||
specifier: ^0.16.8
|
||||
version: 0.16.8(kysely@0.27.4)(pg@8.13.0)
|
||||
nodemon:
|
||||
specifier: ^3.1.7
|
||||
version: 3.1.7
|
||||
prettier:
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
@@ -629,7 +626,7 @@ importers:
|
||||
version: 15.5.2(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
next-auth:
|
||||
specifier: ^4.24.11
|
||||
version: 4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@15.5.2(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.9.15)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
version: 4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@15.5.2(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.9.15)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
next-query-params:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0(next@15.5.2(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)(use-query-params@2.2.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))
|
||||
@@ -1022,9 +1019,6 @@ importers:
|
||||
msw:
|
||||
specifier: ^2.6.5
|
||||
version: 2.6.5(@types/node@24.3.0)(typescript@5.9.2)
|
||||
nodemon:
|
||||
specifier: ^3.1.7
|
||||
version: 3.1.7
|
||||
prettier:
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
@@ -1034,12 +1028,15 @@ importers:
|
||||
tsc-watch:
|
||||
specifier: ^6.2.0
|
||||
version: 6.2.0(typescript@5.9.2)
|
||||
tsx:
|
||||
specifier: ^4.20.5
|
||||
version: 4.20.5
|
||||
typescript:
|
||||
specifier: ^5.7.2
|
||||
version: 5.9.2
|
||||
vitest:
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@20.0.3)(msw@2.6.5(@types/node@24.3.0)(typescript@5.9.2))(terser@5.44.0)(tsx@4.19.1)(yaml@2.5.1)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@20.0.3)(msw@2.6.5(@types/node@24.3.0)(typescript@5.9.2))(terser@5.44.0)(tsx@4.20.5)(yaml@2.5.1)
|
||||
wait-for-expect:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
@@ -1967,204 +1964,102 @@ packages:
|
||||
'@emotion/weak-memoize@0.4.0':
|
||||
resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.23.1':
|
||||
resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.7':
|
||||
resolution: {integrity: sha512-uD0kKFHh6ETr8TqEtaAcV+dn/2qnYbH/+8wGEdY70Qf7l1l/jmBUbrmQqwiPKAQE6cOQ7dTj6Xr0HzQDGHyceQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.23.1':
|
||||
resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm64@0.25.7':
|
||||
resolution: {integrity: sha512-p0ohDnwyIbAtztHTNUTzN5EGD/HJLs1bwysrOPgSdlIA6NDnReoVfoCyxG6W1d85jr2X80Uq5KHftyYgaK9LPQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.23.1':
|
||||
resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.25.7':
|
||||
resolution: {integrity: sha512-Jhuet0g1k9rAJHrXGIh7sFknFuT4sfytYZpZpuZl7YKDhnPByVAm5oy2LEBmMbuYf3ejWVYCc2seX81Mk+madA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.23.1':
|
||||
resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.25.7':
|
||||
resolution: {integrity: sha512-mMxIJFlSgVK23HSsII3ZX9T2xKrBCDGyk0qiZnIW10LLFFtZLkFD6imZHu7gUo2wkNZwS9Yj3mOtZD3ZPcjCcw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.23.1':
|
||||
resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.7':
|
||||
resolution: {integrity: sha512-jyOFLGP2WwRwxM8F1VpP6gcdIJc8jq2CUrURbbTouJoRO7XCkU8GdnTDFIHdcifVBT45cJlOYsZ1kSlfbKjYUQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.23.1':
|
||||
resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.25.7':
|
||||
resolution: {integrity: sha512-m9bVWqZCwQ1BthruifvG64hG03zzz9gE2r/vYAhztBna1/+qXiHyP9WgnyZqHgGeXoimJPhAmxfbeU+nMng6ZA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.23.1':
|
||||
resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.7':
|
||||
resolution: {integrity: sha512-Bss7P4r6uhr3kDzRjPNEnTm/oIBdTPRNQuwaEFWT/uvt6A1YzK/yn5kcx5ZxZ9swOga7LqeYlu7bDIpDoS01bA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.23.1':
|
||||
resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.7':
|
||||
resolution: {integrity: sha512-S3BFyjW81LXG7Vqmr37ddbThrm3A84yE7ey/ERBlK9dIiaWgrjRlre3pbG7txh1Uaxz8N7wGGQXmC9zV+LIpBQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.23.1':
|
||||
resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm64@0.25.7':
|
||||
resolution: {integrity: sha512-HfQZQqrNOfS1Okn7PcsGUqHymL1cWGBslf78dGvtrj8q7cN3FkapFgNA4l/a5lXDwr7BqP2BSO6mz9UremNPbg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.23.1':
|
||||
resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.25.7':
|
||||
resolution: {integrity: sha512-JZMIci/1m5vfQuhKoFXogCKVYVfYQmoZJg8vSIMR4TUXbF+0aNlfXH3DGFEFMElT8hOTUF5hisdZhnrZO/bkDw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.23.1':
|
||||
resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.25.7':
|
||||
resolution: {integrity: sha512-9Jex4uVpdeofiDxnwHRgen+j6398JlX4/6SCbbEFEXN7oMO2p0ueLN+e+9DdsdPLUdqns607HmzEFnxwr7+5wQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.23.1':
|
||||
resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.25.7':
|
||||
resolution: {integrity: sha512-TG1KJqjBlN9IHQjKVUYDB0/mUGgokfhhatlay8aZ/MSORMubEvj/J1CL8YGY4EBcln4z7rKFbsH+HeAv0d471w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.23.1':
|
||||
resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.7':
|
||||
resolution: {integrity: sha512-Ty9Hj/lx7ikTnhOfaP7ipEm/ICcBv94i/6/WDg0OZ3BPBHhChsUbQancoWYSO0WNkEiSW5Do4febTTy4x1qYQQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.23.1':
|
||||
resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.7':
|
||||
resolution: {integrity: sha512-MrOjirGQWGReJl3BNQ58BLhUBPpWABnKrnq8Q/vZWWwAB1wuLXOIxS2JQ1LT3+5T+3jfPh0tyf5CpbyQHqnWIQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.23.1':
|
||||
resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.7':
|
||||
resolution: {integrity: sha512-9pr23/pqzyqIZEZmQXnFyqp3vpa+KBk5TotfkzGMqpw089PGm0AIowkUppHB9derQzqniGn3wVXgck19+oqiOw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.23.1':
|
||||
resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.25.7':
|
||||
resolution: {integrity: sha512-4dP11UVGh9O6Y47m8YvW8eoA3r8qL2toVZUbBKyGta8j6zdw1cn9F/Rt59/Mhv0OgY68pHIMjGXWOUaykCnx+w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.23.1':
|
||||
resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.25.7':
|
||||
resolution: {integrity: sha512-ghJMAJTdw/0uhz7e7YnpdX1xVn7VqA0GrWrAO2qKMuqbvgHT2VZiBv1BQ//VcHsPir4wsL3P2oPggfKPzTKoCA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2177,36 +2072,18 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.23.1':
|
||||
resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.7':
|
||||
resolution: {integrity: sha512-tUZRvLtgLE5OyN46sPSYlgmHoBS5bx2URSrgZdW1L1teWPYVmXh+QN/sKDqkzBo/IHGcKcHLKDhBeVVkO7teEA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.23.1':
|
||||
resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.7':
|
||||
resolution: {integrity: sha512-bTJ50aoC+WDlDGBReWYiObpYvQfMjBNlKztqoNUL0iUkYtwLkBQQeEsTq/I1KyjsKA5tyov6VZaPb8UdD6ci6Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.23.1':
|
||||
resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.7':
|
||||
resolution: {integrity: sha512-TA9XfJrgzAipFUU895jd9j2SyDh9bbNkK2I0gHcvqb/o84UeQkBpi/XmYX3cO1q/9hZokdcDqQxIi6uLVrikxg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2219,48 +2096,24 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.23.1':
|
||||
resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/sunos-x64@0.25.7':
|
||||
resolution: {integrity: sha512-umkbn7KTxsexhv2vuuJmj9kggd4AEtL32KodkJgfhNOHMPtQ55RexsaSrMb+0+jp9XL4I4o2y91PZauVN4cH3A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.23.1':
|
||||
resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-arm64@0.25.7':
|
||||
resolution: {integrity: sha512-j20JQGP/gz8QDgzl5No5Gr4F6hurAZvtkFxAKhiv2X49yi/ih8ECK4Y35YnjlMogSKJk931iNMcd35BtZ4ghfw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.23.1':
|
||||
resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.25.7':
|
||||
resolution: {integrity: sha512-4qZ6NUfoiiKZfLAXRsvFkA0hoWVM+1y2bSHXHkpdLAs/+r0LgwqYohmfZCi985c6JWHhiXP30mgZawn/XrqAkQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.23.1':
|
||||
resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.25.7':
|
||||
resolution: {integrity: sha512-FaPsAHTwm+1Gfvn37Eg3E5HIpfR3i6x1AIcla/MkqAIupD4BW3MrSeUqfoTzwwJhk3WE2/KqUn4/eenEJC76VA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -6009,9 +5862,6 @@ packages:
|
||||
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
|
||||
deprecated: Use your platform's native atob() and btoa() methods instead
|
||||
|
||||
abbrev@1.1.1:
|
||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||
engines: {node: '>=6.5'}
|
||||
@@ -7552,11 +7402,6 @@ packages:
|
||||
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
esbuild@0.23.1:
|
||||
resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
esbuild@0.25.7:
|
||||
resolution: {integrity: sha512-daJB0q2dmTzo90L9NjRaohhRWrCzYxWNFTjEi72/h+p5DcY3yn4MacWfDakHmaBaDzDiuLJsCh0+6LK/iX+c+Q==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -8286,9 +8131,6 @@ packages:
|
||||
get-tsconfig@4.10.1:
|
||||
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
|
||||
|
||||
get-tsconfig@4.8.1:
|
||||
resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==}
|
||||
|
||||
get-uri@6.0.5:
|
||||
resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==}
|
||||
engines: {node: '>= 14'}
|
||||
@@ -8546,9 +8388,6 @@ packages:
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
ignore-by-default@1.0.1:
|
||||
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
|
||||
|
||||
ignore-walk@5.0.1:
|
||||
resolution: {integrity: sha512-yemi4pMf51WKT7khInJqAvsIGzoqYXblnsz0ql8tM+yi1EKYTY1evX4NAbJrLL/Aanr2HyZeluqU+Oi7MGHokw==}
|
||||
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
||||
@@ -10185,15 +10024,6 @@ packages:
|
||||
resolution: {integrity: sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
nodemon@3.1.7:
|
||||
resolution: {integrity: sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
nopt@1.0.10:
|
||||
resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==}
|
||||
hasBin: true
|
||||
|
||||
normalize-package-data@2.5.0:
|
||||
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
|
||||
|
||||
@@ -10924,9 +10754,6 @@ packages:
|
||||
psl@1.9.0:
|
||||
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
|
||||
|
||||
pstree.remy@1.1.8:
|
||||
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
|
||||
|
||||
pump@3.0.0:
|
||||
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
|
||||
|
||||
@@ -11543,10 +11370,6 @@ packages:
|
||||
simple-swizzle@0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
|
||||
simple-update-notifier@2.0.0:
|
||||
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
simple-wcswidth@1.0.1:
|
||||
resolution: {integrity: sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg==}
|
||||
|
||||
@@ -12058,10 +11881,6 @@ packages:
|
||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
touch@3.1.0:
|
||||
resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==}
|
||||
hasBin: true
|
||||
|
||||
tough-cookie@4.1.4:
|
||||
resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -12142,8 +11961,8 @@ packages:
|
||||
peerDependencies:
|
||||
typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
|
||||
|
||||
tsx@4.19.1:
|
||||
resolution: {integrity: sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==}
|
||||
tsx@4.20.5:
|
||||
resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -12290,9 +12109,6 @@ packages:
|
||||
unbzip2-stream@1.4.3:
|
||||
resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==}
|
||||
|
||||
undefsafe@2.0.5:
|
||||
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
||||
|
||||
undici-types@5.26.5:
|
||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||
|
||||
@@ -14756,153 +14572,81 @@ snapshots:
|
||||
|
||||
'@emotion/weak-memoize@0.4.0': {}
|
||||
|
||||
'@esbuild/aix-ppc64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.25.7':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.25.7':
|
||||
optional: true
|
||||
|
||||
@@ -15844,7 +15588,7 @@ snapshots:
|
||||
'@next-auth/prisma-adapter@1.0.7(@prisma/client@6.10.1(prisma@6.10.1(typescript@5.9.2))(typescript@5.9.2))(next-auth@4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@15.5.2(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.9.15)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))':
|
||||
dependencies:
|
||||
'@prisma/client': 6.10.1(prisma@6.10.1(typescript@5.9.2))(typescript@5.9.2)
|
||||
next-auth: 4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@15.5.2(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.9.15)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
next-auth: 4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@15.5.2(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.9.15)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
|
||||
'@next/bundle-analyzer@15.5.2':
|
||||
dependencies:
|
||||
@@ -18633,7 +18377,7 @@ snapshots:
|
||||
'@jest/globals': 29.7.0
|
||||
'@types/jest': 29.5.12
|
||||
jest: 29.7.0(@types/node@24.3.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2))
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@20.0.3)(msw@2.6.5(@types/node@24.3.0)(typescript@5.9.2))(terser@5.44.0)(tsx@4.19.1)(yaml@2.5.1)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@20.0.3)(msw@2.6.5(@types/node@24.3.0)(typescript@5.9.2))(terser@5.44.0)(tsx@4.20.5)(yaml@2.5.1)
|
||||
|
||||
'@testing-library/react@15.0.7(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
|
||||
dependencies:
|
||||
@@ -19344,14 +19088,14 @@ snapshots:
|
||||
chai: 5.2.1
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/mocker@3.2.4(msw@2.6.5(@types/node@24.3.0)(typescript@5.9.2))(vite@7.0.5(@types/node@24.3.0)(jiti@2.5.1)(terser@5.44.0)(tsx@4.19.1)(yaml@2.5.1))':
|
||||
'@vitest/mocker@3.2.4(msw@2.6.5(@types/node@24.3.0)(typescript@5.9.2))(vite@7.0.5(@types/node@24.3.0)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.5.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.17
|
||||
optionalDependencies:
|
||||
msw: 2.6.5(@types/node@24.3.0)(typescript@5.9.2)
|
||||
vite: 7.0.5(@types/node@24.3.0)(jiti@2.5.1)(terser@5.44.0)(tsx@4.19.1)(yaml@2.5.1)
|
||||
vite: 7.0.5(@types/node@24.3.0)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.5.1)
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
@@ -19514,8 +19258,6 @@ snapshots:
|
||||
|
||||
abab@2.0.6: {}
|
||||
|
||||
abbrev@1.1.1: {}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
dependencies:
|
||||
event-target-shim: 5.0.1
|
||||
@@ -20893,11 +20635,9 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
debug@4.3.7(supports-color@5.5.0):
|
||||
debug@4.3.7:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 5.5.0
|
||||
|
||||
debug@4.4.1:
|
||||
dependencies:
|
||||
@@ -21353,34 +21093,6 @@ snapshots:
|
||||
is-date-object: 1.0.5
|
||||
is-symbol: 1.0.4
|
||||
|
||||
esbuild@0.23.1:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.23.1
|
||||
'@esbuild/android-arm': 0.23.1
|
||||
'@esbuild/android-arm64': 0.23.1
|
||||
'@esbuild/android-x64': 0.23.1
|
||||
'@esbuild/darwin-arm64': 0.23.1
|
||||
'@esbuild/darwin-x64': 0.23.1
|
||||
'@esbuild/freebsd-arm64': 0.23.1
|
||||
'@esbuild/freebsd-x64': 0.23.1
|
||||
'@esbuild/linux-arm': 0.23.1
|
||||
'@esbuild/linux-arm64': 0.23.1
|
||||
'@esbuild/linux-ia32': 0.23.1
|
||||
'@esbuild/linux-loong64': 0.23.1
|
||||
'@esbuild/linux-mips64el': 0.23.1
|
||||
'@esbuild/linux-ppc64': 0.23.1
|
||||
'@esbuild/linux-riscv64': 0.23.1
|
||||
'@esbuild/linux-s390x': 0.23.1
|
||||
'@esbuild/linux-x64': 0.23.1
|
||||
'@esbuild/netbsd-x64': 0.23.1
|
||||
'@esbuild/openbsd-arm64': 0.23.1
|
||||
'@esbuild/openbsd-x64': 0.23.1
|
||||
'@esbuild/sunos-x64': 0.23.1
|
||||
'@esbuild/win32-arm64': 0.23.1
|
||||
'@esbuild/win32-ia32': 0.23.1
|
||||
'@esbuild/win32-x64': 0.23.1
|
||||
optional: true
|
||||
|
||||
esbuild@0.25.7:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.7
|
||||
@@ -21510,7 +21222,7 @@ snapshots:
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
fast-glob: 3.3.3
|
||||
get-tsconfig: 4.8.1
|
||||
get-tsconfig: 4.10.1
|
||||
is-core-module: 2.15.1
|
||||
is-glob: 4.0.3
|
||||
transitivePeerDependencies:
|
||||
@@ -21527,7 +21239,7 @@ snapshots:
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0)(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
fast-glob: 3.3.3
|
||||
get-tsconfig: 4.8.1
|
||||
get-tsconfig: 4.10.1
|
||||
is-core-module: 2.15.1
|
||||
is-glob: 4.0.3
|
||||
transitivePeerDependencies:
|
||||
@@ -21544,7 +21256,7 @@ snapshots:
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.9.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
fast-glob: 3.3.3
|
||||
get-tsconfig: 4.8.1
|
||||
get-tsconfig: 4.10.1
|
||||
is-core-module: 2.15.1
|
||||
is-glob: 4.0.3
|
||||
transitivePeerDependencies:
|
||||
@@ -21853,7 +21565,7 @@ snapshots:
|
||||
eslint: 8.57.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.9.2))(eslint@8.57.0)(typescript@5.9.2)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@20.0.3)(msw@2.6.5(@types/node@24.3.0)(typescript@5.9.2))(terser@5.44.0)(tsx@4.19.1)(yaml@2.5.1)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@20.0.3)(msw@2.6.5(@types/node@24.3.0)(typescript@5.9.2))(terser@5.44.0)(tsx@4.20.5)(yaml@2.5.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
@@ -22375,10 +22087,6 @@ snapshots:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
get-tsconfig@4.8.1:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
get-uri@6.0.5:
|
||||
dependencies:
|
||||
basic-ftp: 5.0.5
|
||||
@@ -22693,7 +22401,7 @@ snapshots:
|
||||
https-proxy-agent@7.0.6:
|
||||
dependencies:
|
||||
agent-base: 7.1.3
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -22715,8 +22423,6 @@ snapshots:
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
ignore-by-default@1.0.1: {}
|
||||
|
||||
ignore-walk@5.0.1:
|
||||
dependencies:
|
||||
minimatch: 5.1.6
|
||||
@@ -24658,7 +24364,7 @@ snapshots:
|
||||
dependencies:
|
||||
type-fest: 2.19.0
|
||||
|
||||
next-auth@4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@15.5.2(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.9.15)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
||||
next-auth@4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@15.5.2(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.9.15)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
'@panva/hkdf': 1.1.1
|
||||
@@ -24768,23 +24474,6 @@ snapshots:
|
||||
|
||||
nodemailer@6.9.15: {}
|
||||
|
||||
nodemon@3.1.7:
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
ignore-by-default: 1.0.1
|
||||
minimatch: 3.1.2
|
||||
pstree.remy: 1.1.8
|
||||
semver: 7.6.3
|
||||
simple-update-notifier: 2.0.0
|
||||
supports-color: 5.5.0
|
||||
touch: 3.1.0
|
||||
undefsafe: 2.0.5
|
||||
|
||||
nopt@1.0.10:
|
||||
dependencies:
|
||||
abbrev: 1.1.1
|
||||
|
||||
normalize-package-data@2.5.0:
|
||||
dependencies:
|
||||
hosted-git-info: 2.8.9
|
||||
@@ -25534,8 +25223,6 @@ snapshots:
|
||||
|
||||
psl@1.9.0: {}
|
||||
|
||||
pstree.remy@1.1.8: {}
|
||||
|
||||
pump@3.0.0:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.5
|
||||
@@ -26352,10 +26039,6 @@ snapshots:
|
||||
dependencies:
|
||||
is-arrayish: 0.3.2
|
||||
|
||||
simple-update-notifier@2.0.0:
|
||||
dependencies:
|
||||
semver: 7.7.1
|
||||
|
||||
simple-wcswidth@1.0.1: {}
|
||||
|
||||
sirv@2.0.4:
|
||||
@@ -26921,10 +26604,6 @@ snapshots:
|
||||
|
||||
totalist@3.0.1: {}
|
||||
|
||||
touch@3.1.0:
|
||||
dependencies:
|
||||
nopt: 1.0.10
|
||||
|
||||
tough-cookie@4.1.4:
|
||||
dependencies:
|
||||
psl: 1.9.0
|
||||
@@ -27004,13 +26683,12 @@ snapshots:
|
||||
tslib: 1.14.1
|
||||
typescript: 5.9.2
|
||||
|
||||
tsx@4.19.1:
|
||||
tsx@4.20.5:
|
||||
dependencies:
|
||||
esbuild: 0.23.1
|
||||
esbuild: 0.25.7
|
||||
get-tsconfig: 4.10.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
optional: true
|
||||
|
||||
ttl-set@1.0.0:
|
||||
dependencies:
|
||||
@@ -27169,8 +26847,6 @@ snapshots:
|
||||
buffer: 5.7.1
|
||||
through: 2.3.8
|
||||
|
||||
undefsafe@2.0.5: {}
|
||||
|
||||
undici-types@5.26.5: {}
|
||||
|
||||
undici-types@7.10.0: {}
|
||||
@@ -27393,13 +27069,13 @@ snapshots:
|
||||
'@egjs/hammerjs': 2.0.17
|
||||
component-emitter: 1.3.1
|
||||
|
||||
vite-node@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(terser@5.44.0)(tsx@4.19.1)(yaml@2.5.1):
|
||||
vite-node@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.5.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.1
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 7.0.5(@types/node@24.3.0)(jiti@2.5.1)(terser@5.44.0)(tsx@4.19.1)(yaml@2.5.1)
|
||||
vite: 7.0.5(@types/node@24.3.0)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.5.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
@@ -27414,7 +27090,7 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite@7.0.5(@types/node@24.3.0)(jiti@2.5.1)(terser@5.44.0)(tsx@4.19.1)(yaml@2.5.1):
|
||||
vite@7.0.5(@types/node@24.3.0)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.5.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.7
|
||||
fdir: 6.4.6(picomatch@4.0.3)
|
||||
@@ -27427,14 +27103,14 @@ snapshots:
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.5.1
|
||||
terser: 5.44.0
|
||||
tsx: 4.19.1
|
||||
tsx: 4.20.5
|
||||
yaml: 2.5.1
|
||||
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@20.0.3)(msw@2.6.5(@types/node@24.3.0)(typescript@5.9.2))(terser@5.44.0)(tsx@4.19.1)(yaml@2.5.1):
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@20.0.3)(msw@2.6.5(@types/node@24.3.0)(typescript@5.9.2))(terser@5.44.0)(tsx@4.20.5)(yaml@2.5.1):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.2
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(msw@2.6.5(@types/node@24.3.0)(typescript@5.9.2))(vite@7.0.5(@types/node@24.3.0)(jiti@2.5.1)(terser@5.44.0)(tsx@4.19.1)(yaml@2.5.1))
|
||||
'@vitest/mocker': 3.2.4(msw@2.6.5(@types/node@24.3.0)(typescript@5.9.2))(vite@7.0.5(@types/node@24.3.0)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.5.1))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
@@ -27452,8 +27128,8 @@ snapshots:
|
||||
tinyglobby: 0.2.14
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 7.0.5(@types/node@24.3.0)(jiti@2.5.1)(terser@5.44.0)(tsx@4.19.1)(yaml@2.5.1)
|
||||
vite-node: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(terser@5.44.0)(tsx@4.19.1)(yaml@2.5.1)
|
||||
vite: 7.0.5(@types/node@24.3.0)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.5.1)
|
||||
vite-node: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(terser@5.44.0)(tsx@4.20.5)(yaml@2.5.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/debug': 4.1.12
|
||||
|
||||
+36
-10
@@ -1,15 +1,26 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"globalDependencies": [".env"],
|
||||
"globalDependencies": [
|
||||
".env"
|
||||
],
|
||||
"envMode": "loose",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["db:generate", "^build"],
|
||||
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
|
||||
"dependsOn": [
|
||||
"db:generate",
|
||||
"^build"
|
||||
],
|
||||
"outputs": [
|
||||
"dist/**",
|
||||
".next/**",
|
||||
"!.next/cache/**"
|
||||
],
|
||||
"cache": true
|
||||
},
|
||||
"start": {
|
||||
"dependsOn": ["^start"]
|
||||
"dependsOn": [
|
||||
"^start"
|
||||
]
|
||||
},
|
||||
"db:migrate": {
|
||||
"cache": false
|
||||
@@ -22,29 +33,44 @@
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true,
|
||||
"dependsOn": ["db:generate"]
|
||||
"dependsOn": [
|
||||
"db:generate"
|
||||
]
|
||||
},
|
||||
"dev:worker": {
|
||||
"cache": false,
|
||||
"persistent": true,
|
||||
"dependsOn": ["db:generate"]
|
||||
"dependsOn": [
|
||||
"db:generate",
|
||||
]
|
||||
},
|
||||
"dev:web": {
|
||||
"cache": false,
|
||||
"persistent": true,
|
||||
"dependsOn": ["db:generate"]
|
||||
"dependsOn": [
|
||||
"db:generate"
|
||||
]
|
||||
},
|
||||
"db:generate": {
|
||||
"cache": false,
|
||||
"dependsOn": ["^db:generate"]
|
||||
"dependsOn": [
|
||||
"^db:generate"
|
||||
]
|
||||
},
|
||||
"lint": {
|
||||
"cache": true,
|
||||
"outputs": []
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["^test", "db:generate"],
|
||||
"dependsOn": [
|
||||
"^test",
|
||||
"db:generate"
|
||||
],
|
||||
"cache": true
|
||||
},
|
||||
"worker#dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "3.109.0",
|
||||
"version": "3.110.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -560,6 +560,146 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AnnotationQueueAssignmentRequest'
|
||||
/api/public/integrations/blob-storage:
|
||||
get:
|
||||
description: >-
|
||||
Get all blob storage integrations for the organization (requires
|
||||
organization-scoped API key)
|
||||
operationId: blobStorageIntegrations_getBlobStorageIntegrations
|
||||
tags:
|
||||
- BlobStorageIntegrations
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BlobStorageIntegrationsResponse'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security:
|
||||
- BasicAuth: []
|
||||
put:
|
||||
description: >-
|
||||
Create or update a blob storage integration for a specific project
|
||||
(requires organization-scoped API key). The configuration is validated
|
||||
by performing a test upload to the bucket.
|
||||
operationId: blobStorageIntegrations_upsertBlobStorageIntegration
|
||||
tags:
|
||||
- BlobStorageIntegrations
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BlobStorageIntegrationResponse'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security:
|
||||
- BasicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateBlobStorageIntegrationRequest'
|
||||
/api/public/integrations/blob-storage/{id}:
|
||||
delete:
|
||||
description: >-
|
||||
Delete a blob storage integration by ID (requires organization-scoped
|
||||
API key)
|
||||
operationId: blobStorageIntegrations_deleteBlobStorageIntegration
|
||||
tags:
|
||||
- BlobStorageIntegrations
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BlobStorageIntegrationDeletionResponse'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/comments:
|
||||
post:
|
||||
description: >-
|
||||
@@ -1440,7 +1580,7 @@ paths:
|
||||
Notes:
|
||||
|
||||
- Introduction to data model:
|
||||
https://langfuse.com/docs/tracing-data-model
|
||||
https://langfuse.com/docs/observability/data-model
|
||||
|
||||
- Batch sizes are limited to 3.5 MB in total. You need to adjust the
|
||||
number of events per batch accordingly.
|
||||
@@ -4413,9 +4553,11 @@ paths:
|
||||
in: query
|
||||
description: >-
|
||||
Comma-separated list of fields to include in the response. Available
|
||||
field groups are 'core' (always included), 'io' (input, output,
|
||||
metadata), 'scores', 'observations', 'metrics'. If not provided, all
|
||||
fields are included. Example: 'core,scores,metrics'
|
||||
field groups: 'core' (always included), 'io' (input, output,
|
||||
metadata), 'scores', 'observations', 'metrics'. If not specified,
|
||||
all fields are returned. Example: 'core,scores,metrics'. Note:
|
||||
Excluded 'observations' or 'scores' fields return empty arrays;
|
||||
excluded 'metrics' returns -1 for 'totalCost' and 'latency'.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -4686,6 +4828,178 @@ components:
|
||||
- userId
|
||||
- queueId
|
||||
- projectId
|
||||
BlobStorageIntegrationType:
|
||||
title: BlobStorageIntegrationType
|
||||
type: string
|
||||
enum:
|
||||
- S3
|
||||
- S3_COMPATIBLE
|
||||
- AZURE_BLOB_STORAGE
|
||||
BlobStorageIntegrationFileType:
|
||||
title: BlobStorageIntegrationFileType
|
||||
type: string
|
||||
enum:
|
||||
- JSON
|
||||
- CSV
|
||||
- JSONL
|
||||
BlobStorageExportMode:
|
||||
title: BlobStorageExportMode
|
||||
type: string
|
||||
enum:
|
||||
- FULL_HISTORY
|
||||
- FROM_TODAY
|
||||
- FROM_CUSTOM_DATE
|
||||
BlobStorageExportFrequency:
|
||||
title: BlobStorageExportFrequency
|
||||
type: string
|
||||
enum:
|
||||
- hourly
|
||||
- daily
|
||||
- weekly
|
||||
CreateBlobStorageIntegrationRequest:
|
||||
title: CreateBlobStorageIntegrationRequest
|
||||
type: object
|
||||
properties:
|
||||
projectId:
|
||||
type: string
|
||||
description: ID of the project in which to configure the blob storage integration
|
||||
type:
|
||||
$ref: '#/components/schemas/BlobStorageIntegrationType'
|
||||
bucketName:
|
||||
type: string
|
||||
description: Name of the storage bucket
|
||||
endpoint:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Custom endpoint URL (required for S3_COMPATIBLE type)
|
||||
region:
|
||||
type: string
|
||||
description: Storage region
|
||||
accessKeyId:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Access key ID for authentication
|
||||
secretAccessKey:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Secret access key for authentication (will be encrypted when stored)
|
||||
prefix:
|
||||
type: string
|
||||
nullable: true
|
||||
description: >-
|
||||
Path prefix for exported files (must end with forward slash if
|
||||
provided)
|
||||
exportFrequency:
|
||||
$ref: '#/components/schemas/BlobStorageExportFrequency'
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Whether the integration is active
|
||||
forcePathStyle:
|
||||
type: boolean
|
||||
description: Use path-style URLs for S3 requests
|
||||
fileType:
|
||||
$ref: '#/components/schemas/BlobStorageIntegrationFileType'
|
||||
exportMode:
|
||||
$ref: '#/components/schemas/BlobStorageExportMode'
|
||||
exportStartDate:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: >-
|
||||
Custom start date for exports (required when exportMode is
|
||||
FROM_CUSTOM_DATE)
|
||||
required:
|
||||
- projectId
|
||||
- type
|
||||
- bucketName
|
||||
- region
|
||||
- exportFrequency
|
||||
- enabled
|
||||
- forcePathStyle
|
||||
- fileType
|
||||
- exportMode
|
||||
BlobStorageIntegrationResponse:
|
||||
title: BlobStorageIntegrationResponse
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
projectId:
|
||||
type: string
|
||||
type:
|
||||
$ref: '#/components/schemas/BlobStorageIntegrationType'
|
||||
bucketName:
|
||||
type: string
|
||||
endpoint:
|
||||
type: string
|
||||
nullable: true
|
||||
region:
|
||||
type: string
|
||||
accessKeyId:
|
||||
type: string
|
||||
nullable: true
|
||||
prefix:
|
||||
type: string
|
||||
exportFrequency:
|
||||
$ref: '#/components/schemas/BlobStorageExportFrequency'
|
||||
enabled:
|
||||
type: boolean
|
||||
forcePathStyle:
|
||||
type: boolean
|
||||
fileType:
|
||||
$ref: '#/components/schemas/BlobStorageIntegrationFileType'
|
||||
exportMode:
|
||||
$ref: '#/components/schemas/BlobStorageExportMode'
|
||||
exportStartDate:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
nextSyncAt:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
lastSyncAt:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
required:
|
||||
- id
|
||||
- projectId
|
||||
- type
|
||||
- bucketName
|
||||
- region
|
||||
- prefix
|
||||
- exportFrequency
|
||||
- enabled
|
||||
- forcePathStyle
|
||||
- fileType
|
||||
- exportMode
|
||||
- createdAt
|
||||
- updatedAt
|
||||
BlobStorageIntegrationsResponse:
|
||||
title: BlobStorageIntegrationsResponse
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/BlobStorageIntegrationResponse'
|
||||
required:
|
||||
- data
|
||||
BlobStorageIntegrationDeletionResponse:
|
||||
title: BlobStorageIntegrationDeletionResponse
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
required:
|
||||
- message
|
||||
CreateCommentRequest:
|
||||
title: CreateCommentRequest
|
||||
type: object
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,619 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import {
|
||||
makeZodVerifiedAPICall,
|
||||
makeAPICall,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { z } from "zod/v4";
|
||||
import { randomUUID } from "crypto";
|
||||
import {
|
||||
createAndAddApiKeysToDb,
|
||||
createBasicAuthHeader,
|
||||
} from "@langfuse/shared/src/server";
|
||||
|
||||
// Schemas based on Fern schema definition
|
||||
const BlobStorageIntegrationResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
projectId: z.string(),
|
||||
type: z.enum(["S3", "S3_COMPATIBLE", "AZURE_BLOB_STORAGE"]),
|
||||
bucketName: z.string(),
|
||||
endpoint: z.string().nullable(),
|
||||
region: z.string(),
|
||||
accessKeyId: z.string().nullable(),
|
||||
prefix: z.string(),
|
||||
exportFrequency: z.enum(["hourly", "daily", "weekly"]),
|
||||
enabled: z.boolean(),
|
||||
forcePathStyle: z.boolean(),
|
||||
fileType: z.enum(["JSON", "CSV", "JSONL"]),
|
||||
exportMode: z.enum(["FULL_HISTORY", "FROM_TODAY", "FROM_CUSTOM_DATE"]),
|
||||
exportStartDate: z.coerce.date().nullable(),
|
||||
nextSyncAt: z.coerce.date().nullable(),
|
||||
lastSyncAt: z.coerce.date().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
const BlobStorageIntegrationsResponseSchema = z.object({
|
||||
data: z.array(BlobStorageIntegrationResponseSchema),
|
||||
});
|
||||
|
||||
const BlobStorageIntegrationDeletionResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
// Valid blob storage integration request payload
|
||||
const validBlobStorageConfig = {
|
||||
projectId: "",
|
||||
type: "S3" as const,
|
||||
bucketName: "test-bucket",
|
||||
endpoint: null,
|
||||
region: "us-east-1",
|
||||
accessKeyId: "AKIA123456789",
|
||||
secretAccessKey: "secret123456789",
|
||||
prefix: "langfuse-exports/",
|
||||
exportFrequency: "daily" as const,
|
||||
enabled: true,
|
||||
forcePathStyle: false,
|
||||
fileType: "JSONL" as const,
|
||||
exportMode: "FULL_HISTORY" as const,
|
||||
exportStartDate: null,
|
||||
};
|
||||
|
||||
describe("Blob Storage Integrations API", () => {
|
||||
// Test data
|
||||
let testOrgId: string;
|
||||
let testProject1Id: string;
|
||||
let testProject2Id: string;
|
||||
let testApiKey: string;
|
||||
let testApiSecretKey: string;
|
||||
let otherOrgId: string;
|
||||
let otherProjectId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create test organization
|
||||
const testOrg = await prisma.organization.create({
|
||||
data: {
|
||||
name: `Blob Storage Test Org ${randomUUID().substring(0, 8)}`,
|
||||
cloudConfig: { plan: "Team" },
|
||||
},
|
||||
});
|
||||
testOrgId = testOrg.id;
|
||||
|
||||
// Create test projects
|
||||
const testProject1 = await prisma.project.create({
|
||||
data: {
|
||||
name: `Blob Storage Test Project 1 ${randomUUID().substring(0, 8)}`,
|
||||
orgId: testOrgId,
|
||||
},
|
||||
});
|
||||
testProject1Id = testProject1.id;
|
||||
|
||||
const testProject2 = await prisma.project.create({
|
||||
data: {
|
||||
name: `Blob Storage Test Project 2 ${randomUUID().substring(0, 8)}`,
|
||||
orgId: testOrgId,
|
||||
},
|
||||
});
|
||||
testProject2Id = testProject2.id;
|
||||
|
||||
// Create organization API key
|
||||
const orgApiKey = await createAndAddApiKeysToDb({
|
||||
prisma,
|
||||
entityId: testOrgId,
|
||||
scope: "ORGANIZATION",
|
||||
note: "Test API Key for Blob Storage API",
|
||||
predefinedKeys: {
|
||||
publicKey: `pk-lf-blob-${randomUUID().substring(0, 8)}`,
|
||||
secretKey: `sk-lf-blob-${randomUUID().substring(0, 8)}`,
|
||||
},
|
||||
});
|
||||
testApiKey = orgApiKey.publicKey;
|
||||
testApiSecretKey = orgApiKey.secretKey;
|
||||
|
||||
// Create another organization for cross-org tests
|
||||
const otherOrg = await prisma.organization.create({
|
||||
data: {
|
||||
name: `Other Blob Storage Org ${randomUUID().substring(0, 8)}`,
|
||||
cloudConfig: { plan: "Team" },
|
||||
},
|
||||
});
|
||||
otherOrgId = otherOrg.id;
|
||||
|
||||
const otherProject = await prisma.project.create({
|
||||
data: {
|
||||
name: `Other Blob Storage Project ${randomUUID().substring(0, 8)}`,
|
||||
orgId: otherOrgId,
|
||||
},
|
||||
});
|
||||
otherProjectId = otherProject.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up test data
|
||||
await prisma.organization.delete({
|
||||
where: { id: testOrgId },
|
||||
});
|
||||
await prisma.organization.delete({
|
||||
where: { id: otherOrgId },
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/public/integrations/blob-storage", () => {
|
||||
let testIntegrationId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create a test blob storage integration
|
||||
const integration = await prisma.blobStorageIntegration.create({
|
||||
data: {
|
||||
projectId: testProject1Id,
|
||||
type: "S3",
|
||||
bucketName: "test-bucket",
|
||||
region: "us-east-1",
|
||||
accessKeyId: "test-access-key",
|
||||
secretAccessKey: "encrypted-secret",
|
||||
prefix: "langfuse-exports/",
|
||||
exportFrequency: "daily",
|
||||
enabled: true,
|
||||
forcePathStyle: false,
|
||||
fileType: "JSONL",
|
||||
exportMode: "FULL_HISTORY",
|
||||
},
|
||||
});
|
||||
testIntegrationId = integration.projectId;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up test integration
|
||||
await prisma.blobStorageIntegration.deleteMany({
|
||||
where: { projectId: testProject1Id },
|
||||
});
|
||||
});
|
||||
|
||||
it("should get all blob storage integrations for the organization", async () => {
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
BlobStorageIntegrationsResponseSchema,
|
||||
"GET",
|
||||
"/api/public/integrations/blob-storage",
|
||||
undefined,
|
||||
createBasicAuthHeader(testApiKey, testApiSecretKey),
|
||||
200,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
expect(response.body.data.length).toBe(1);
|
||||
|
||||
const integration = response.body.data.find(
|
||||
(i) => i.id === testIntegrationId,
|
||||
);
|
||||
expect(integration).toBeDefined();
|
||||
expect(integration?.projectId).toBe(testProject1Id);
|
||||
expect(integration?.type).toBe("S3");
|
||||
expect(integration?.bucketName).toBe("test-bucket");
|
||||
expect(integration?.accessKeyId).toBe("test-access-key");
|
||||
// Verify that secretAccessKey is not returned
|
||||
expect(integration).not.toHaveProperty("secretAccessKey");
|
||||
});
|
||||
|
||||
it("should return 401 with invalid API key", async () => {
|
||||
const result = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/integrations/blob-storage",
|
||||
undefined,
|
||||
createBasicAuthHeader("invalid-key", "invalid-secret"),
|
||||
);
|
||||
expect(result.status).toBe(401);
|
||||
});
|
||||
|
||||
it("should return 403 with project-scoped API key", async () => {
|
||||
// Create project API key
|
||||
const projectApiKey = await createAndAddApiKeysToDb({
|
||||
prisma,
|
||||
entityId: testProject1Id,
|
||||
scope: "PROJECT",
|
||||
note: "Project API Key",
|
||||
predefinedKeys: {
|
||||
publicKey: `pk-lf-proj-${randomUUID().substring(0, 8)}`,
|
||||
secretKey: `sk-lf-proj-${randomUUID().substring(0, 8)}`,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/integrations/blob-storage",
|
||||
undefined,
|
||||
createBasicAuthHeader(projectApiKey.publicKey, projectApiKey.secretKey),
|
||||
);
|
||||
expect(result.status).toBe(403);
|
||||
expect(result.body.message).toContain(
|
||||
"Organization-scoped API key required",
|
||||
);
|
||||
|
||||
// Clean up
|
||||
await prisma.apiKey.delete({ where: { id: projectApiKey.id } });
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/public/integrations/blob-storage", () => {
|
||||
afterEach(async () => {
|
||||
// Clean up any created integrations
|
||||
await prisma.blobStorageIntegration.deleteMany({
|
||||
where: {
|
||||
projectId: {
|
||||
in: [testProject1Id, testProject2Id],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a new blob storage integration", async () => {
|
||||
const requestBody = {
|
||||
...validBlobStorageConfig,
|
||||
projectId: testProject1Id,
|
||||
};
|
||||
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
BlobStorageIntegrationResponseSchema,
|
||||
"PUT",
|
||||
"/api/public/integrations/blob-storage",
|
||||
requestBody,
|
||||
createBasicAuthHeader(testApiKey, testApiSecretKey),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.projectId).toBe(testProject1Id);
|
||||
expect(response.body.type).toBe("S3");
|
||||
expect(response.body.bucketName).toBe("test-bucket");
|
||||
expect(response.body.enabled).toBe(true);
|
||||
// Verify secretAccessKey is not returned
|
||||
expect(response.body).not.toHaveProperty("secretAccessKey");
|
||||
|
||||
// Verify it was saved to database
|
||||
const savedIntegration = await prisma.blobStorageIntegration.findUnique({
|
||||
where: { projectId: testProject1Id },
|
||||
});
|
||||
expect(savedIntegration).toBeDefined();
|
||||
expect(savedIntegration?.bucketName).toBe("test-bucket");
|
||||
});
|
||||
|
||||
it("should update an existing blob storage integration", async () => {
|
||||
// Create initial integration
|
||||
await prisma.blobStorageIntegration.create({
|
||||
data: {
|
||||
projectId: testProject1Id,
|
||||
type: "S3",
|
||||
bucketName: "old-bucket",
|
||||
region: "us-west-1",
|
||||
accessKeyId: "old-key",
|
||||
secretAccessKey: "old-secret",
|
||||
prefix: "old-prefix/",
|
||||
exportFrequency: "hourly",
|
||||
enabled: false,
|
||||
forcePathStyle: true,
|
||||
fileType: "JSON",
|
||||
exportMode: "FROM_TODAY",
|
||||
},
|
||||
});
|
||||
|
||||
const updateBody = {
|
||||
...validBlobStorageConfig,
|
||||
projectId: testProject1Id,
|
||||
bucketName: "updated-bucket",
|
||||
enabled: true,
|
||||
exportFrequency: "weekly" as const,
|
||||
};
|
||||
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
BlobStorageIntegrationResponseSchema,
|
||||
"PUT",
|
||||
"/api/public/integrations/blob-storage",
|
||||
updateBody,
|
||||
createBasicAuthHeader(testApiKey, testApiSecretKey),
|
||||
200,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.bucketName).toBe("updated-bucket");
|
||||
expect(response.body.enabled).toBe(true);
|
||||
expect(response.body.exportFrequency).toBe("weekly");
|
||||
});
|
||||
|
||||
it("should validate required fields", async () => {
|
||||
const invalidBody = {
|
||||
projectId: testProject1Id,
|
||||
type: "S3",
|
||||
// Missing bucketName
|
||||
region: "us-east-1",
|
||||
};
|
||||
|
||||
const result = await makeAPICall(
|
||||
"PUT",
|
||||
"/api/public/integrations/blob-storage",
|
||||
invalidBody,
|
||||
createBasicAuthHeader(testApiKey, testApiSecretKey),
|
||||
);
|
||||
expect(result.status).toBe(400);
|
||||
expect(result.body.message).toContain("Invalid request data");
|
||||
});
|
||||
|
||||
it("should validate enum values", async () => {
|
||||
const invalidBody = {
|
||||
...validBlobStorageConfig,
|
||||
projectId: testProject1Id,
|
||||
type: "INVALID_TYPE",
|
||||
};
|
||||
|
||||
const result = await makeAPICall(
|
||||
"PUT",
|
||||
"/api/public/integrations/blob-storage",
|
||||
invalidBody,
|
||||
createBasicAuthHeader(testApiKey, testApiSecretKey),
|
||||
);
|
||||
expect(result.status).toBe(400);
|
||||
expect(result.body.message).toContain("Invalid request data");
|
||||
});
|
||||
|
||||
it("should validate prefix format", async () => {
|
||||
const invalidBody = {
|
||||
...validBlobStorageConfig,
|
||||
projectId: testProject1Id,
|
||||
prefix: "invalid-prefix", // Should end with /
|
||||
};
|
||||
|
||||
const result = await makeAPICall(
|
||||
"PUT",
|
||||
"/api/public/integrations/blob-storage",
|
||||
invalidBody,
|
||||
createBasicAuthHeader(testApiKey, testApiSecretKey),
|
||||
);
|
||||
expect(result.status).toBe(400);
|
||||
expect(result.body.error).toBeDefined();
|
||||
});
|
||||
|
||||
it("should return 404 for non-existent project", async () => {
|
||||
const nonExistentProjectId = randomUUID();
|
||||
const requestBody = {
|
||||
...validBlobStorageConfig,
|
||||
projectId: nonExistentProjectId,
|
||||
};
|
||||
|
||||
const result = await makeAPICall(
|
||||
"PUT",
|
||||
"/api/public/integrations/blob-storage",
|
||||
requestBody,
|
||||
createBasicAuthHeader(testApiKey, testApiSecretKey),
|
||||
);
|
||||
expect(result.status).toBe(404);
|
||||
expect(result.body.message).toContain("Project not found");
|
||||
});
|
||||
|
||||
it("should return 404 for project from different organization", async () => {
|
||||
const requestBody = {
|
||||
...validBlobStorageConfig,
|
||||
projectId: otherProjectId,
|
||||
};
|
||||
|
||||
const result = await makeAPICall(
|
||||
"PUT",
|
||||
"/api/public/integrations/blob-storage",
|
||||
requestBody,
|
||||
createBasicAuthHeader(testApiKey, testApiSecretKey),
|
||||
);
|
||||
expect(result.status).toBe(404);
|
||||
expect(result.body.message).toContain("Project not found");
|
||||
});
|
||||
|
||||
it("should return 401 with invalid API key", async () => {
|
||||
const requestBody = {
|
||||
...validBlobStorageConfig,
|
||||
projectId: testProject1Id,
|
||||
};
|
||||
|
||||
const result = await makeAPICall(
|
||||
"PUT",
|
||||
"/api/public/integrations/blob-storage",
|
||||
requestBody,
|
||||
createBasicAuthHeader("invalid-key", "invalid-secret"),
|
||||
);
|
||||
expect(result.status).toBe(401);
|
||||
});
|
||||
|
||||
it("should handle different blob storage types", async () => {
|
||||
const azureConfig = {
|
||||
...validBlobStorageConfig,
|
||||
projectId: testProject1Id,
|
||||
type: "AZURE_BLOB_STORAGE" as const,
|
||||
endpoint: "https://myaccount.blob.core.windows.net",
|
||||
};
|
||||
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
BlobStorageIntegrationResponseSchema,
|
||||
"PUT",
|
||||
"/api/public/integrations/blob-storage",
|
||||
azureConfig,
|
||||
createBasicAuthHeader(testApiKey, testApiSecretKey),
|
||||
200,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.type).toBe("AZURE_BLOB_STORAGE");
|
||||
expect(response.body.endpoint).toBe(
|
||||
"https://myaccount.blob.core.windows.net",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle export modes with dates", async () => {
|
||||
const customDateConfig = {
|
||||
...validBlobStorageConfig,
|
||||
projectId: testProject1Id,
|
||||
exportMode: "FROM_CUSTOM_DATE" as const,
|
||||
exportStartDate: "2024-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
BlobStorageIntegrationResponseSchema,
|
||||
"PUT",
|
||||
"/api/public/integrations/blob-storage",
|
||||
customDateConfig,
|
||||
createBasicAuthHeader(testApiKey, testApiSecretKey),
|
||||
200,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.exportMode).toBe("FROM_CUSTOM_DATE");
|
||||
expect(response.body.exportStartDate).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/public/integrations/blob-storage/{id}", () => {
|
||||
let testIntegrationId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create test integration
|
||||
const integration = await prisma.blobStorageIntegration.create({
|
||||
data: {
|
||||
projectId: testProject1Id,
|
||||
type: "S3",
|
||||
bucketName: "test-bucket",
|
||||
region: "us-east-1",
|
||||
accessKeyId: "test-key",
|
||||
secretAccessKey: "test-secret",
|
||||
prefix: "test/",
|
||||
exportFrequency: "daily",
|
||||
enabled: true,
|
||||
forcePathStyle: false,
|
||||
fileType: "JSONL",
|
||||
exportMode: "FULL_HISTORY",
|
||||
},
|
||||
});
|
||||
testIntegrationId = integration.projectId; // Based on current implementation, ID is projectId
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up
|
||||
await prisma.blobStorageIntegration.deleteMany({
|
||||
where: { projectId: testProject1Id },
|
||||
});
|
||||
});
|
||||
|
||||
it("should delete a blob storage integration", async () => {
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
BlobStorageIntegrationDeletionResponseSchema,
|
||||
"DELETE",
|
||||
`/api/public/integrations/blob-storage/${testIntegrationId}`,
|
||||
undefined,
|
||||
createBasicAuthHeader(testApiKey, testApiSecretKey),
|
||||
200,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toBeDefined();
|
||||
|
||||
// Verify it was deleted from database
|
||||
const deletedIntegration = await prisma.blobStorageIntegration.findUnique(
|
||||
{
|
||||
where: { projectId: testIntegrationId },
|
||||
},
|
||||
);
|
||||
expect(deletedIntegration).toBeNull();
|
||||
});
|
||||
|
||||
it("should return 404 for non-existent integration", async () => {
|
||||
const nonExistentId = randomUUID();
|
||||
const result = await makeAPICall(
|
||||
"DELETE",
|
||||
`/api/public/integrations/blob-storage/${nonExistentId}`,
|
||||
undefined,
|
||||
createBasicAuthHeader(testApiKey, testApiSecretKey),
|
||||
);
|
||||
expect(result.status).toBe(404);
|
||||
});
|
||||
|
||||
it("should return 404 for integration from different organization", async () => {
|
||||
// Create integration in other org
|
||||
const otherOrgIntegration = await prisma.blobStorageIntegration.create({
|
||||
data: {
|
||||
projectId: otherProjectId,
|
||||
type: "S3",
|
||||
bucketName: "other-bucket",
|
||||
region: "us-east-1",
|
||||
accessKeyId: "other-key",
|
||||
secretAccessKey: "other-secret",
|
||||
prefix: "other/",
|
||||
exportFrequency: "daily",
|
||||
enabled: true,
|
||||
forcePathStyle: false,
|
||||
fileType: "JSONL",
|
||||
exportMode: "FULL_HISTORY",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await makeAPICall(
|
||||
"DELETE",
|
||||
`/api/public/integrations/blob-storage/${otherOrgIntegration.projectId}`,
|
||||
undefined,
|
||||
createBasicAuthHeader(testApiKey, testApiSecretKey),
|
||||
);
|
||||
expect(result.status).toBe(404);
|
||||
|
||||
// Check config still exists
|
||||
const integrations = await prisma.blobStorageIntegration.findMany({
|
||||
where: { projectId: otherProjectId },
|
||||
});
|
||||
expect(integrations).toHaveLength(1);
|
||||
|
||||
// Clean up
|
||||
await prisma.blobStorageIntegration.delete({
|
||||
where: { projectId: otherProjectId },
|
||||
});
|
||||
});
|
||||
|
||||
it("should return 401 with invalid API key", async () => {
|
||||
const result = await makeAPICall(
|
||||
"DELETE",
|
||||
`/api/public/integrations/blob-storage/${testIntegrationId}`,
|
||||
undefined,
|
||||
createBasicAuthHeader("invalid-key", "invalid-secret"),
|
||||
);
|
||||
expect(result.status).toBe(401);
|
||||
});
|
||||
|
||||
it("should return 403 with project-scoped API key", async () => {
|
||||
const projectApiKey = await createAndAddApiKeysToDb({
|
||||
prisma,
|
||||
entityId: testProject1Id,
|
||||
scope: "PROJECT",
|
||||
note: "Project API Key",
|
||||
predefinedKeys: {
|
||||
publicKey: `pk-lf-proj-del-${randomUUID().substring(0, 8)}`,
|
||||
secretKey: `sk-lf-proj-del-${randomUUID().substring(0, 8)}`,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await makeAPICall(
|
||||
"DELETE",
|
||||
`/api/public/integrations/blob-storage/${testIntegrationId}`,
|
||||
undefined,
|
||||
createBasicAuthHeader(projectApiKey.publicKey, projectApiKey.secretKey),
|
||||
);
|
||||
expect(result.status).toBe(403);
|
||||
|
||||
// Clean up
|
||||
await prisma.apiKey.delete({ where: { id: projectApiKey.id } });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Method validation", () => {
|
||||
it("should return 405 for unsupported methods", async () => {
|
||||
const result = await makeAPICall(
|
||||
"PATCH",
|
||||
"/api/public/integrations/blob-storage",
|
||||
undefined,
|
||||
createBasicAuthHeader(testApiKey, testApiSecretKey),
|
||||
);
|
||||
expect(result.status).toBe(405);
|
||||
expect(result.body.message).toContain("Method not allowed");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -492,6 +492,7 @@ describe("OTel Resource Span Mapping", () => {
|
||||
environment: "production",
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw an error if langfuse scope spans have wrong project ID", async () => {
|
||||
const langfuseOtelSpans = [
|
||||
{
|
||||
@@ -2019,6 +2020,30 @@ describe("OTel Resource Span Mapping", () => {
|
||||
entityAttributeValue: '{"foo": "bar"}',
|
||||
},
|
||||
],
|
||||
[
|
||||
"should map gcp.vertex.agent.tool_call_args to input",
|
||||
{
|
||||
entity: "observation",
|
||||
otelAttributeKey: "gcp.vertex.agent.tool_call_args",
|
||||
otelAttributeValue: {
|
||||
stringValue: '{"foo": "bar"}',
|
||||
},
|
||||
entityAttributeKey: "input",
|
||||
entityAttributeValue: '{"foo": "bar"}',
|
||||
},
|
||||
],
|
||||
[
|
||||
"should map gcp.vertex.agent.tool_response to output",
|
||||
{
|
||||
entity: "observation",
|
||||
otelAttributeKey: "gcp.vertex.agent.tool_response",
|
||||
otelAttributeValue: {
|
||||
stringValue: '{"foo": "bar"}',
|
||||
},
|
||||
entityAttributeKey: "output",
|
||||
entityAttributeValue: '{"foo": "bar"}',
|
||||
},
|
||||
],
|
||||
])(
|
||||
"Attributes: %s",
|
||||
async (
|
||||
|
||||
@@ -29,6 +29,15 @@ type SidebarNotification = {
|
||||
};
|
||||
|
||||
const notifications: SidebarNotification[] = [
|
||||
{
|
||||
id: "js-sdk-v4",
|
||||
title: "New JS/TS SDK v4",
|
||||
description:
|
||||
"With v4, the TypeScript SDK significantly improves DX, speed, and ecosystem integrations.",
|
||||
link: "https://langfuse.com/docs/observability/sdk/typescript/overview",
|
||||
linkTitle: "Learn more",
|
||||
createdAt: "2025-09-09",
|
||||
},
|
||||
{
|
||||
id: "python-sdk-v3",
|
||||
title: "New Python SDK v3",
|
||||
|
||||
@@ -28,6 +28,11 @@ import { ScrollArea } from "@/src/components/ui/scroll-area";
|
||||
import { Label } from "@/src/components/ui/label";
|
||||
import { AnnotationQueueObjectType, type APIScoreV2 } from "@langfuse/shared";
|
||||
import { CreateNewAnnotationQueueItem } from "@/src/features/annotation-queues/components/CreateNewAnnotationQueueItem";
|
||||
import { TablePeekView } from "@/src/components/table/peek";
|
||||
import { PeekViewTraceDetail } from "@/src/components/table/peek/peek-trace-detail";
|
||||
import { usePeekNavigation } from "@/src/components/table/peek/hooks/usePeekNavigation";
|
||||
import { NewDatasetItemFromExistingObject } from "@/src/features/datasets/components/NewDatasetItemFromExistingObject";
|
||||
import { ItemBadge } from "@/src/components/ItemBadge";
|
||||
|
||||
// some projects have thousands of traces in a sessions, paginate to avoid rendering all at once
|
||||
const PAGE_SIZE = 50;
|
||||
@@ -153,11 +158,27 @@ export const SessionPage: React.FC<{
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { openPeek, closePeek, resolveDetailNavigationPath, expandPeek } =
|
||||
usePeekNavigation({
|
||||
expandConfig: {
|
||||
// Expand peeked traces to the trace detail route; sessions list traces
|
||||
basePath: `/project/${projectId}/traces`,
|
||||
},
|
||||
queryParams: ["timestamp"],
|
||||
extractParamsValuesFromRow: (row: any) => ({
|
||||
timestamp: row.timestamp.toISOString(),
|
||||
}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (session.isSuccess) {
|
||||
setDetailPageList(
|
||||
"traces",
|
||||
session.data.traces.map((t) => ({ id: t.id })),
|
||||
session.data.traces.map((t) => ({
|
||||
id: t.id,
|
||||
params: { timestamp: t.timestamp.toISOString() },
|
||||
})),
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -202,8 +223,6 @@ export const SessionPage: React.FC<{
|
||||
|
||||
return (
|
||||
<Page
|
||||
withPadding
|
||||
scrollable
|
||||
headerProps={{
|
||||
title: sessionId,
|
||||
itemType: "SESSION",
|
||||
@@ -280,90 +299,136 @@ export const SessionPage: React.FC<{
|
||||
),
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
<SessionUsers projectId={projectId} users={session.data?.users} />
|
||||
<Badge variant="outline">Traces: {session.data?.traces.length}</Badge>
|
||||
{session.data && (
|
||||
<div className="flex h-full flex-col overflow-auto">
|
||||
<div className="sticky top-0 z-40 flex flex-wrap gap-2 border-b bg-background p-4">
|
||||
{session.data?.users?.length ? (
|
||||
<SessionUsers projectId={projectId} users={session.data.users} />
|
||||
) : null}
|
||||
<Badge variant="outline">
|
||||
Total cost: {usdFormatter(session.data.totalCost, 2)}
|
||||
Total traces: {session.data?.traces.length}
|
||||
</Badge>
|
||||
)}
|
||||
<SessionScores
|
||||
scores={
|
||||
session.data?.scores?.map((score) => ({
|
||||
...score,
|
||||
timestamp: new Date(score.timestamp),
|
||||
createdAt: new Date(score.createdAt),
|
||||
updatedAt: new Date(score.updatedAt),
|
||||
})) ?? []
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5 flex flex-col gap-2 border-t pt-5">
|
||||
{session.data?.traces.slice(0, visibleTraces).map((trace) => (
|
||||
<Card
|
||||
className="group grid gap-3 border-border p-2 shadow-none hover:border-ring md:grid-cols-3"
|
||||
key={trace.id}
|
||||
>
|
||||
<div className="col-span-2 overflow-hidden">
|
||||
<SessionIO
|
||||
traceId={trace.id}
|
||||
projectId={projectId}
|
||||
timestamp={new Date(trace.timestamp)}
|
||||
/>
|
||||
</div>
|
||||
<div className="-mt-1 p-1 opacity-50 transition-opacity group-hover:opacity-100">
|
||||
<Link
|
||||
href={`/project/${projectId}/traces/${trace.id}`}
|
||||
className="text-xs hover:underline"
|
||||
{session.data && (
|
||||
<Badge variant="outline">
|
||||
Total cost: {usdFormatter(session.data.totalCost, 2)}
|
||||
</Badge>
|
||||
)}
|
||||
<SessionScores
|
||||
scores={
|
||||
session.data?.scores?.map((score) => ({
|
||||
...score,
|
||||
timestamp: new Date(score.timestamp),
|
||||
createdAt: new Date(score.createdAt),
|
||||
updatedAt: new Date(score.updatedAt),
|
||||
})) ?? []
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
{session.data?.traces.slice(0, visibleTraces).map((trace) => (
|
||||
<Card className="border-border shadow-none" key={trace.id}>
|
||||
<div className="grid md:grid-cols-[1fr_1px_358px] lg:grid-cols-[1fr_1px_28rem]">
|
||||
<div className="overflow-hidden py-4 pl-4 pr-4">
|
||||
<SessionIO
|
||||
traceId={trace.id}
|
||||
projectId={projectId}
|
||||
timestamp={new Date(trace.timestamp)}
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden bg-border md:block"></div>
|
||||
<div className="flex flex-col border-t py-4 pl-4 pr-4 md:border-0">
|
||||
<div className="mb-4 flex flex-col gap-2">
|
||||
<Link
|
||||
href={`/project/${projectId}/traces/${trace.id}`}
|
||||
className="flex items-start gap-2 rounded-lg border p-2 transition-colors hover:bg-accent"
|
||||
onClick={(e) => {
|
||||
// Only prevent default for normal clicks, allow modifier key clicks through
|
||||
if (!e.metaKey && !e.ctrlKey && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
openPeek(trace.id, trace);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ItemBadge type="TRACE" isSmall />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-medium">
|
||||
{trace.name} ({trace.id}) ↗
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{trace.timestamp.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<NewDatasetItemFromTraceId
|
||||
projectId={projectId}
|
||||
traceId={trace.id}
|
||||
timestamp={new Date(trace.timestamp)}
|
||||
buttonVariant="outline"
|
||||
/>
|
||||
<AnnotateDrawer
|
||||
projectId={projectId}
|
||||
scoreTarget={{
|
||||
type: "trace",
|
||||
traceId: trace.id,
|
||||
}}
|
||||
scores={trace.scores}
|
||||
emptySelectedConfigIds={emptySelectedConfigIds}
|
||||
setEmptySelectedConfigIds={setEmptySelectedConfigIds}
|
||||
variant="button"
|
||||
buttonVariant="outline"
|
||||
analyticsData={{
|
||||
type: "trace",
|
||||
source: "SessionDetail",
|
||||
}}
|
||||
key={"annotation-drawer" + trace.id}
|
||||
environment={trace.environment}
|
||||
/>
|
||||
<CommentDrawerButton
|
||||
projectId={projectId}
|
||||
variant="outline"
|
||||
objectId={trace.id}
|
||||
objectType="TRACE"
|
||||
count={getNumberFromMap(
|
||||
traceCommentCounts.data,
|
||||
trace.id,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="mb-1 font-medium">Scores</p>
|
||||
<div className="flex flex-wrap content-start items-start gap-1">
|
||||
<GroupedScoreBadges scores={trace.scores} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
{session.data?.traces &&
|
||||
session.data.traces.length > visibleTraces && (
|
||||
<Button
|
||||
onClick={() => setVisibleTraces((prev) => prev + PAGE_SIZE)}
|
||||
variant="ghost"
|
||||
className="self-center"
|
||||
>
|
||||
Trace: {trace.name} ({trace.id}) ↗
|
||||
</Link>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{trace.timestamp.toLocaleString()}
|
||||
</div>
|
||||
<div className="mb-1 mt-2 text-xs text-muted-foreground">
|
||||
Scores
|
||||
</div>
|
||||
<div className="mb-1 flex flex-wrap content-start items-start gap-1">
|
||||
<GroupedScoreBadges scores={trace.scores} />
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<AnnotateDrawer
|
||||
projectId={projectId}
|
||||
scoreTarget={{
|
||||
type: "trace",
|
||||
traceId: trace.id,
|
||||
}}
|
||||
scores={trace.scores}
|
||||
emptySelectedConfigIds={emptySelectedConfigIds}
|
||||
setEmptySelectedConfigIds={setEmptySelectedConfigIds}
|
||||
variant="badge"
|
||||
analyticsData={{ type: "trace", source: "SessionDetail" }}
|
||||
key={"annotation-drawer" + trace.id}
|
||||
environment={trace.environment}
|
||||
/>
|
||||
<CommentDrawerButton
|
||||
projectId={projectId}
|
||||
objectId={trace.id}
|
||||
objectType="TRACE"
|
||||
count={getNumberFromMap(traceCommentCounts.data, trace.id)}
|
||||
className="h-6 rounded-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
{session.data?.traces && session.data.traces.length > visibleTraces && (
|
||||
<Button
|
||||
onClick={() => setVisibleTraces((prev) => prev + PAGE_SIZE)}
|
||||
variant="ghost"
|
||||
className="self-center"
|
||||
>
|
||||
{`Load ${Math.min(session.data.traces.length - visibleTraces, PAGE_SIZE)} More`}
|
||||
</Button>
|
||||
)}
|
||||
{`Load ${Math.min(session.data.traces.length - visibleTraces, PAGE_SIZE)} More`}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<TablePeekView
|
||||
peekView={{
|
||||
itemType: "TRACE",
|
||||
detailNavigationKey: "traces",
|
||||
openPeek,
|
||||
closePeek,
|
||||
expandPeek,
|
||||
resolveDetailNavigationPath,
|
||||
children: <PeekViewTraceDetail projectId={projectId} />,
|
||||
tableDataUpdatedAt: session.dataUpdatedAt,
|
||||
}}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -411,3 +476,41 @@ export const SessionIO = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NewDatasetItemFromTraceId = (props: {
|
||||
projectId: string;
|
||||
traceId: string;
|
||||
timestamp: Date;
|
||||
buttonVariant?: "outline" | "secondary";
|
||||
}) => {
|
||||
// SessionIO already fetches the trace, so this doesn't add an extra request
|
||||
const trace = api.traces.byId.useQuery(
|
||||
{
|
||||
traceId: props.traceId,
|
||||
projectId: props.projectId,
|
||||
timestamp: props.timestamp,
|
||||
},
|
||||
{
|
||||
enabled: typeof props.traceId === "string",
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
refetchOnMount: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (!trace.data) return null;
|
||||
|
||||
return (
|
||||
<NewDatasetItemFromExistingObject
|
||||
projectId={props.projectId}
|
||||
traceId={props.traceId}
|
||||
input={trace.data.input ?? null}
|
||||
output={trace.data.output ?? null}
|
||||
metadata={trace.data.metadata ?? null}
|
||||
buttonVariant={props.buttonVariant}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,15 @@ type UsePeekRunsCompareDataProps = {
|
||||
datasetItemId?: string;
|
||||
traceId?: string;
|
||||
timestamp?: Date;
|
||||
runs?: string[];
|
||||
runs?: string[] | string;
|
||||
};
|
||||
|
||||
// Ensure runs is always an array - handle case where URL param comes as string
|
||||
const safeParseUrlParamToArray = (
|
||||
runs?: string[] | string,
|
||||
): string[] | undefined => {
|
||||
if (!runs) return undefined;
|
||||
return Array.isArray(runs) ? runs : [runs];
|
||||
};
|
||||
|
||||
export const usePeekRunsCompareData = ({
|
||||
@@ -17,6 +25,8 @@ export const usePeekRunsCompareData = ({
|
||||
datasetItemId,
|
||||
runs,
|
||||
}: UsePeekRunsCompareDataProps) => {
|
||||
const parsedRuns = safeParseUrlParamToArray(runs);
|
||||
|
||||
const trace = api.traces.byIdWithObservationsAndScores.useQuery(
|
||||
{
|
||||
traceId: traceId as string,
|
||||
@@ -49,10 +59,11 @@ export const usePeekRunsCompareData = ({
|
||||
projectId,
|
||||
datasetId: datasetId as string,
|
||||
datasetItemId: datasetItemId as string,
|
||||
datasetRunIds: runs as string[] | undefined,
|
||||
datasetRunIds: parsedRuns,
|
||||
},
|
||||
{
|
||||
enabled: !!datasetId && !!datasetItemId && !!runs && runs.length > 0,
|
||||
enabled:
|
||||
!!datasetId && !!datasetItemId && !!parsedRuns && parsedRuns.length > 0,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { SubHeaderLabel } from "@/src/components/layouts/header";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import useLocalStorage from "@/src/components/useLocalStorage";
|
||||
import usePreserveRelativeScroll from "@/src/hooks/usePreserveRelativeScroll";
|
||||
|
||||
export const IOPreview: React.FC<{
|
||||
input?: Prisma.JsonValue;
|
||||
@@ -55,6 +56,8 @@ export const IOPreview: React.FC<{
|
||||
const capture = usePostHogClientCapture();
|
||||
const input = deepParseJson(props.input);
|
||||
const output = deepParseJson(props.output);
|
||||
const [compensateScrollRef, startPreserveScroll] =
|
||||
usePreserveRelativeScroll<HTMLDivElement>([selectedView]);
|
||||
|
||||
// parse old completions: { completion: string } -> string
|
||||
const outLegacyCompletionSchema = z
|
||||
@@ -118,14 +121,16 @@ export const IOPreview: React.FC<{
|
||||
{isPrettyViewAvailable && !currentView ? (
|
||||
<div className="flex w-full flex-row justify-start">
|
||||
<Tabs
|
||||
ref={compensateScrollRef}
|
||||
className="h-fit py-0.5"
|
||||
value={selectedView}
|
||||
onValueChange={(value) => {
|
||||
startPreserveScroll();
|
||||
capture("trace_detail:io_mode_switch", { view: value });
|
||||
setLocalCurrentView(value as "pretty" | "json");
|
||||
}}
|
||||
>
|
||||
<TabsList className="h-fit py-0.5">
|
||||
<TabsList className="h-fit p-0.5">
|
||||
<TabsTrigger value="pretty" className="h-fit px-1 text-xs">
|
||||
Formatted
|
||||
</TabsTrigger>
|
||||
|
||||
@@ -15,15 +15,17 @@ const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-[9999] overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-[9999] overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v3.109.0";
|
||||
export const VERSION = "v3.110.0";
|
||||
|
||||
@@ -56,7 +56,6 @@ export const stripeProducts: StripeProduct[] = [
|
||||
"Unlimited annotation queues",
|
||||
"High rate limits",
|
||||
"SOC2, ISO27001 reports",
|
||||
"Support via Slack",
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -76,6 +75,7 @@ export const stripeProducts: StripeProduct[] = [
|
||||
"SSO enforcement",
|
||||
"Fine-grained RBAC",
|
||||
"Data retention management",
|
||||
"Support via Slack",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -99,6 +99,7 @@ export function DatasetRunItemsByRunTable(props: {
|
||||
header: "Dataset Item",
|
||||
id: "datasetItemId",
|
||||
size: 110,
|
||||
isPinnedLeft: true,
|
||||
cell: ({ row }) => {
|
||||
const datasetItemId: string = row.getValue("datasetItemId");
|
||||
return (
|
||||
|
||||
@@ -43,6 +43,7 @@ export const NewDatasetItemFromExistingObject = (props: {
|
||||
output: string | null;
|
||||
metadata: Prisma.JsonValue;
|
||||
isCopyItem?: boolean;
|
||||
buttonVariant?: "outline" | "secondary";
|
||||
}) => {
|
||||
const parsedInput =
|
||||
props.input && typeof props.input === "string"
|
||||
@@ -74,6 +75,7 @@ export const NewDatasetItemFromExistingObject = (props: {
|
||||
scope: "datasets:CUD",
|
||||
});
|
||||
const capture = usePostHogClientCapture();
|
||||
const buttonVariant = props.buttonVariant || "secondary";
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -137,7 +139,7 @@ export const NewDatasetItemFromExistingObject = (props: {
|
||||
object: props.observationId ? "observation" : "trace",
|
||||
});
|
||||
}}
|
||||
variant="secondary"
|
||||
variant={buttonVariant}
|
||||
disabled={!hasAccess}
|
||||
>
|
||||
{hasAccess ? (
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
/**
|
||||
* Enums
|
||||
*/
|
||||
|
||||
export const BlobStorageIntegrationType = z.enum([
|
||||
"S3",
|
||||
"S3_COMPATIBLE",
|
||||
"AZURE_BLOB_STORAGE",
|
||||
]);
|
||||
|
||||
export const BlobStorageIntegrationFileType = z.enum(["JSON", "CSV", "JSONL"]);
|
||||
|
||||
export const BlobStorageExportMode = z.enum([
|
||||
"FULL_HISTORY",
|
||||
"FROM_TODAY",
|
||||
"FROM_CUSTOM_DATE",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Request/Response Types
|
||||
*/
|
||||
|
||||
export const CreateBlobStorageIntegrationRequest = z
|
||||
.object({
|
||||
projectId: z.string(),
|
||||
type: BlobStorageIntegrationType,
|
||||
bucketName: z.string(),
|
||||
endpoint: z.string().nullable().optional(),
|
||||
region: z.string(),
|
||||
accessKeyId: z.string().nullable().optional(),
|
||||
secretAccessKey: z.string().nullable().optional(),
|
||||
prefix: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("")
|
||||
.refine(
|
||||
(value) => value === "" || value.endsWith("/"),
|
||||
"Prefix must be empty or end with a forward slash",
|
||||
),
|
||||
exportFrequency: z.string(),
|
||||
enabled: z.boolean(),
|
||||
forcePathStyle: z.boolean(),
|
||||
fileType: BlobStorageIntegrationFileType,
|
||||
exportMode: BlobStorageExportMode,
|
||||
exportStartDate: z.coerce.date().nullable().optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine(
|
||||
(data) => {
|
||||
return !(data.exportMode === "FROM_CUSTOM_DATE" && !data.exportStartDate);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"exportStartDate is required when exportMode is FROM_CUSTOM_DATE",
|
||||
path: ["exportStartDate"],
|
||||
},
|
||||
);
|
||||
|
||||
export const BlobStorageIntegrationResponse = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
projectId: z.string(),
|
||||
type: BlobStorageIntegrationType,
|
||||
bucketName: z.string(),
|
||||
endpoint: z.string().nullable(),
|
||||
region: z.string(),
|
||||
accessKeyId: z.string().nullable(),
|
||||
prefix: z.string(),
|
||||
exportFrequency: z.string(),
|
||||
enabled: z.boolean(),
|
||||
forcePathStyle: z.boolean(),
|
||||
fileType: BlobStorageIntegrationFileType,
|
||||
exportMode: BlobStorageExportMode,
|
||||
exportStartDate: z.coerce.date().nullable(),
|
||||
nextSyncAt: z.coerce.date().nullable(),
|
||||
lastSyncAt: z.coerce.date().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type BlobStorageIntegrationResponseType = z.infer<
|
||||
typeof BlobStorageIntegrationResponse
|
||||
>;
|
||||
@@ -0,0 +1,171 @@
|
||||
import { useCallback, useLayoutEffect, useRef } from "react";
|
||||
|
||||
type ScrollTarget = Window | Element;
|
||||
|
||||
function isWindow(target: ScrollTarget): target is Window {
|
||||
return (
|
||||
(target as Window).scrollBy !== undefined &&
|
||||
(target as Window).document !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
function getComputedOverflowY(node: Element): string {
|
||||
const style = window.getComputedStyle(node);
|
||||
return style.overflowY;
|
||||
}
|
||||
|
||||
function isScrollable(node: Element): boolean {
|
||||
const overflowY = getComputedOverflowY(node);
|
||||
if (overflowY !== "auto" && overflowY !== "scroll") return false;
|
||||
return node.scrollHeight > node.clientHeight;
|
||||
}
|
||||
|
||||
function findNearestScrollContainer(start: Element): ScrollTarget {
|
||||
let node: Element | null = start;
|
||||
while (node && node !== document.body) {
|
||||
if (isScrollable(node)) return node;
|
||||
node = node.parentElement;
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
export interface UsePreserveRelativeScrollOptions {
|
||||
getScrollTarget?: (clickedElement: Element) => ScrollTarget;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preserves the referenced element's relative scroll position when content size changes.
|
||||
*
|
||||
* @param options - Optional configuration for scroll target detection and enabling the behavior.
|
||||
* @param layoutDeps - Values that change when the layout will reflow due to your interaction
|
||||
* (for example, the selected tab value). Provide stable, memoized values; avoid passing
|
||||
* freshly created objects or inline functions.
|
||||
*/
|
||||
export function usePreserveRelativeScroll<T extends Element = Element>(
|
||||
layoutDeps: ReadonlyArray<unknown> = [],
|
||||
options?: UsePreserveRelativeScrollOptions,
|
||||
): [React.RefObject<T | null>, () => void] {
|
||||
const enabled = options?.enabled ?? true;
|
||||
const beforeTopRef = useRef<number | null>(null);
|
||||
const targetRef = useRef<ScrollTarget | null>(null);
|
||||
const didUserScrollRef = useRef<boolean>(false);
|
||||
const elementRef = useRef<T | null>(null);
|
||||
const compensatedRef = useRef<boolean>(false);
|
||||
|
||||
const attachScrollListener = useCallback(() => {
|
||||
const target = targetRef.current;
|
||||
const cancel = () => {
|
||||
didUserScrollRef.current = true;
|
||||
};
|
||||
const keydownHandler = (e: KeyboardEvent) => {
|
||||
const keys = [
|
||||
"ArrowUp",
|
||||
"ArrowDown",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
"Home",
|
||||
"End",
|
||||
" ",
|
||||
];
|
||||
if (keys.includes(e.key)) cancel();
|
||||
};
|
||||
window.addEventListener("wheel", cancel, { passive: true, once: true });
|
||||
window.addEventListener("touchmove", cancel, { passive: true, once: true });
|
||||
window.addEventListener(
|
||||
"keydown",
|
||||
keydownHandler as EventListener,
|
||||
{
|
||||
once: true,
|
||||
} as AddEventListenerOptions,
|
||||
);
|
||||
if (target && !isWindow(target)) {
|
||||
target.addEventListener(
|
||||
"wheel",
|
||||
cancel as EventListener,
|
||||
{
|
||||
passive: true,
|
||||
once: true,
|
||||
} as AddEventListenerOptions,
|
||||
);
|
||||
target.addEventListener(
|
||||
"touchmove",
|
||||
cancel as EventListener,
|
||||
{
|
||||
passive: true,
|
||||
once: true,
|
||||
} as AddEventListenerOptions,
|
||||
);
|
||||
target.addEventListener(
|
||||
"keydown",
|
||||
keydownHandler as EventListener,
|
||||
{
|
||||
once: true,
|
||||
} as AddEventListenerOptions,
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const removeRefs = useCallback(() => {
|
||||
beforeTopRef.current = null;
|
||||
targetRef.current = null;
|
||||
didUserScrollRef.current = false;
|
||||
compensatedRef.current = false;
|
||||
}, []);
|
||||
|
||||
const performCompensation = useCallback(
|
||||
(element: T) => {
|
||||
if (compensatedRef.current) return;
|
||||
const beforeTop = beforeTopRef.current;
|
||||
const target = targetRef.current;
|
||||
if (beforeTop == null || !target) return;
|
||||
if (didUserScrollRef.current) {
|
||||
removeRefs();
|
||||
return;
|
||||
}
|
||||
const afterTop = element.getBoundingClientRect().top;
|
||||
const delta = afterTop - beforeTop;
|
||||
if (Math.abs(delta) < 1) {
|
||||
removeRefs();
|
||||
return;
|
||||
}
|
||||
if (isWindow(target)) {
|
||||
window.scrollBy({ top: delta, left: 0 });
|
||||
} else {
|
||||
(target as Element).scrollTop += delta;
|
||||
}
|
||||
compensatedRef.current = true;
|
||||
removeRefs();
|
||||
},
|
||||
[removeRefs],
|
||||
);
|
||||
|
||||
const startPreserveScroll = useCallback(() => {
|
||||
if (!enabled) return;
|
||||
const element = elementRef.current;
|
||||
if (!element || !element.getBoundingClientRect) return;
|
||||
const rect = element.getBoundingClientRect();
|
||||
beforeTopRef.current = rect.top;
|
||||
targetRef.current =
|
||||
options?.getScrollTarget?.(element) ??
|
||||
findNearestScrollContainer(element);
|
||||
didUserScrollRef.current = false;
|
||||
attachScrollListener();
|
||||
}, [attachScrollListener, enabled, options]);
|
||||
|
||||
const compensateInLayout = useCallback(() => {
|
||||
if (!enabled) return;
|
||||
const element = elementRef.current;
|
||||
if (!element) return;
|
||||
performCompensation(element);
|
||||
}, [enabled, performCompensation]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
compensateInLayout();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, layoutDeps);
|
||||
|
||||
return [elementRef, startPreserveScroll];
|
||||
}
|
||||
|
||||
export default usePreserveRelativeScroll;
|
||||
@@ -0,0 +1,79 @@
|
||||
import { ApiAuthService } from "@/src/features/public-api/server/apiAuth";
|
||||
import { withMiddlewares } from "@/src/features/public-api/server/withMiddlewares";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { redis } from "@langfuse/shared/src/server";
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
import { hasEntitlementBasedOnPlan } from "@/src/features/entitlements/server/hasEntitlement";
|
||||
import {
|
||||
LangfuseNotFoundError,
|
||||
UnauthorizedError,
|
||||
ForbiddenError,
|
||||
} from "@langfuse/shared";
|
||||
|
||||
export default withMiddlewares({
|
||||
DELETE: handleDeleteBlobStorageIntegration,
|
||||
});
|
||||
|
||||
async function handleDeleteBlobStorageIntegration(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
// CHECK AUTH
|
||||
const authCheck = await new ApiAuthService(
|
||||
prisma,
|
||||
redis,
|
||||
).verifyAuthHeaderAndReturnScope(req.headers.authorization);
|
||||
if (!authCheck.validKey) {
|
||||
throw new UnauthorizedError(authCheck.error ?? "Unauthorized");
|
||||
}
|
||||
|
||||
// Check if using an organization API key
|
||||
if (
|
||||
authCheck.scope.accessLevel !== "organization" ||
|
||||
!authCheck.scope.orgId
|
||||
) {
|
||||
throw new ForbiddenError(
|
||||
"Organization-scoped API key required for this operation.",
|
||||
);
|
||||
}
|
||||
|
||||
// Check scheduled-blob-exports entitlement
|
||||
if (
|
||||
!hasEntitlementBasedOnPlan({
|
||||
plan: authCheck.scope.plan,
|
||||
entitlement: "scheduled-blob-exports",
|
||||
})
|
||||
) {
|
||||
throw new ForbiddenError(
|
||||
"scheduled-blob-exports entitlement required for this feature.",
|
||||
);
|
||||
}
|
||||
const { id } = req.query;
|
||||
|
||||
if (!id || typeof id !== "string") {
|
||||
throw new Error("Invalid integration ID");
|
||||
}
|
||||
|
||||
// Check if the integration exists and belongs to a project in the organization
|
||||
const integration = await prisma.blobStorageIntegration.findUnique({
|
||||
where: { projectId: id },
|
||||
include: {
|
||||
project: {
|
||||
select: { orgId: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!integration || integration.project.orgId !== authCheck.scope.orgId) {
|
||||
throw new LangfuseNotFoundError("Blob storage integration not found");
|
||||
}
|
||||
|
||||
// Delete the integration
|
||||
await prisma.blobStorageIntegration.delete({
|
||||
where: { projectId: id },
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
message: "Blob storage integration successfully deleted",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import { ApiAuthService } from "@/src/features/public-api/server/apiAuth";
|
||||
import { withMiddlewares } from "@/src/features/public-api/server/withMiddlewares";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { redis } from "@langfuse/shared/src/server";
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
import { hasEntitlementBasedOnPlan } from "@/src/features/entitlements/server/hasEntitlement";
|
||||
import {
|
||||
CreateBlobStorageIntegrationRequest,
|
||||
type BlobStorageIntegrationResponseType,
|
||||
} from "@/src/features/public-api/types/blob-storage-integrations";
|
||||
import {
|
||||
LangfuseNotFoundError,
|
||||
UnauthorizedError,
|
||||
ForbiddenError,
|
||||
} from "@langfuse/shared";
|
||||
|
||||
export default withMiddlewares({
|
||||
GET: handleGetBlobStorageIntegrations,
|
||||
PUT: handleUpsertBlobStorageIntegration,
|
||||
});
|
||||
|
||||
async function handleGetBlobStorageIntegrations(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
// CHECK AUTH
|
||||
const authCheck = await new ApiAuthService(
|
||||
prisma,
|
||||
redis,
|
||||
).verifyAuthHeaderAndReturnScope(req.headers.authorization);
|
||||
if (!authCheck.validKey) {
|
||||
throw new UnauthorizedError(authCheck.error ?? "Unauthorized");
|
||||
}
|
||||
|
||||
// Check if using an organization API key
|
||||
if (
|
||||
authCheck.scope.accessLevel !== "organization" ||
|
||||
!authCheck.scope.orgId
|
||||
) {
|
||||
throw new ForbiddenError(
|
||||
"Organization-scoped API key required for this operation.",
|
||||
);
|
||||
}
|
||||
|
||||
// Check scheduled-blob-exports entitlement
|
||||
if (
|
||||
!hasEntitlementBasedOnPlan({
|
||||
plan: authCheck.scope.plan,
|
||||
entitlement: "scheduled-blob-exports",
|
||||
})
|
||||
) {
|
||||
throw new ForbiddenError(
|
||||
"scheduled-blob-exports entitlement required for this feature.",
|
||||
);
|
||||
}
|
||||
|
||||
// Get all projects for the organization
|
||||
const projects = await prisma.project.findMany({
|
||||
where: { orgId: authCheck.scope.orgId },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
// Get all blob storage integrations for these projects
|
||||
const integrations = await prisma.blobStorageIntegration.findMany({
|
||||
where: {
|
||||
projectId: { in: projects.map((p) => p.id) },
|
||||
},
|
||||
});
|
||||
|
||||
// Transform to API response format, exclude secretAccessKey
|
||||
const responseData: BlobStorageIntegrationResponseType[] = integrations.map(
|
||||
(integration) => ({
|
||||
id: integration.projectId, // Using projectId as ID since it's the primary key
|
||||
projectId: integration.projectId,
|
||||
type: integration.type,
|
||||
bucketName: integration.bucketName,
|
||||
endpoint: integration.endpoint,
|
||||
region: integration.region,
|
||||
accessKeyId: integration.accessKeyId,
|
||||
prefix: integration.prefix,
|
||||
exportFrequency: integration.exportFrequency,
|
||||
enabled: integration.enabled,
|
||||
forcePathStyle: integration.forcePathStyle,
|
||||
fileType: integration.fileType,
|
||||
exportMode: integration.exportMode,
|
||||
exportStartDate: integration.exportStartDate,
|
||||
nextSyncAt: integration.nextSyncAt,
|
||||
lastSyncAt: integration.lastSyncAt,
|
||||
createdAt: integration.createdAt,
|
||||
updatedAt: integration.updatedAt,
|
||||
}),
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: responseData,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleUpsertBlobStorageIntegration(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
// CHECK AUTH
|
||||
const authCheck = await new ApiAuthService(
|
||||
prisma,
|
||||
redis,
|
||||
).verifyAuthHeaderAndReturnScope(req.headers.authorization);
|
||||
if (!authCheck.validKey) {
|
||||
throw new UnauthorizedError(authCheck.error ?? "Unauthorized");
|
||||
}
|
||||
|
||||
// Check if using an organization API key
|
||||
if (
|
||||
authCheck.scope.accessLevel !== "organization" ||
|
||||
!authCheck.scope.orgId
|
||||
) {
|
||||
throw new ForbiddenError(
|
||||
"Organization-scoped API key required for this operation.",
|
||||
);
|
||||
}
|
||||
|
||||
// Check scheduled-blob-exports entitlement
|
||||
if (
|
||||
!hasEntitlementBasedOnPlan({
|
||||
plan: authCheck.scope.plan,
|
||||
entitlement: "scheduled-blob-exports",
|
||||
})
|
||||
) {
|
||||
throw new ForbiddenError(
|
||||
"scheduled-blob-exports entitlement required for this feature.",
|
||||
);
|
||||
}
|
||||
|
||||
// Validate request body
|
||||
const validatedData = CreateBlobStorageIntegrationRequest.parse(req.body);
|
||||
|
||||
// Check if the project exists and belongs to the organization
|
||||
const project = await prisma.project.findUnique({
|
||||
where: { id: validatedData.projectId },
|
||||
select: { id: true, orgId: true },
|
||||
});
|
||||
if (!project || project.orgId !== authCheck.scope.orgId) {
|
||||
throw new LangfuseNotFoundError("Project not found");
|
||||
}
|
||||
|
||||
// Prepare data for database
|
||||
const dbData = {
|
||||
projectId: validatedData.projectId,
|
||||
type: validatedData.type,
|
||||
bucketName: validatedData.bucketName,
|
||||
endpoint: validatedData.endpoint || null,
|
||||
region: validatedData.region,
|
||||
accessKeyId: validatedData.accessKeyId || null,
|
||||
secretAccessKey: validatedData.secretAccessKey || null,
|
||||
prefix: validatedData.prefix,
|
||||
exportFrequency: validatedData.exportFrequency,
|
||||
enabled: validatedData.enabled,
|
||||
forcePathStyle: validatedData.forcePathStyle,
|
||||
fileType: validatedData.fileType,
|
||||
exportMode: validatedData.exportMode,
|
||||
exportStartDate: validatedData.exportStartDate || null,
|
||||
};
|
||||
|
||||
// Upsert the integration (create or update)
|
||||
const integration = await prisma.blobStorageIntegration.upsert({
|
||||
where: { projectId: validatedData.projectId },
|
||||
update: dbData,
|
||||
create: dbData,
|
||||
});
|
||||
|
||||
// Transform to API response format, exclude secretAccessKey
|
||||
const responseData: BlobStorageIntegrationResponseType = {
|
||||
id: integration.projectId, // Using projectId as ID since it's the primary key
|
||||
projectId: integration.projectId,
|
||||
type: integration.type,
|
||||
bucketName: integration.bucketName,
|
||||
endpoint: integration.endpoint,
|
||||
region: integration.region,
|
||||
accessKeyId: integration.accessKeyId,
|
||||
prefix: integration.prefix,
|
||||
exportFrequency: integration.exportFrequency,
|
||||
enabled: integration.enabled,
|
||||
forcePathStyle: integration.forcePathStyle,
|
||||
fileType: integration.fileType,
|
||||
exportMode: integration.exportMode,
|
||||
exportStartDate: integration.exportStartDate,
|
||||
nextSyncAt: integration.nextSyncAt,
|
||||
lastSyncAt: integration.lastSyncAt,
|
||||
createdAt: integration.createdAt,
|
||||
updatedAt: integration.updatedAt,
|
||||
};
|
||||
|
||||
return res.status(200).json(responseData);
|
||||
}
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "worker",
|
||||
"version": "3.109.0",
|
||||
"version": "3.110.0",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -12,7 +12,7 @@
|
||||
"coverage": "vitest run --coverage",
|
||||
"start": "dotenv -e ../.env -- node dist/index.js",
|
||||
"build": "tsc",
|
||||
"dev": "dotenv -e ../.env -- nodemon src/index.ts",
|
||||
"dev": "dotenv -e ../.env -- tsx watch --clear-screen=false --include '../packages/shared/dist/*' src/index.ts",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
|
||||
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
||||
"refill-ingestion-events": "dotenv -e ../.env -- tsx src/scripts/replayIngestionEvents/s3-ingestion-event-replay.ts",
|
||||
@@ -77,10 +77,10 @@
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"kysely-codegen": "^0.16.8",
|
||||
"msw": "^2.6.5",
|
||||
"nodemon": "^3.1.7",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-watch": "^6.2.0",
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "^5.7.2",
|
||||
"vitest": "^3.2.4",
|
||||
"wait-for-expect": "^3.0.2"
|
||||
|
||||
@@ -112,6 +112,11 @@ if (env.QUEUE_CONSUMER_CREATE_EVAL_QUEUE_IS_ENABLED === "true") {
|
||||
evalJobCreatorQueueProcessor,
|
||||
{
|
||||
concurrency: env.LANGFUSE_EVAL_CREATOR_WORKER_CONCURRENCY,
|
||||
limiter: {
|
||||
// Process at most `max` jobs per 2 seconds globally
|
||||
max: env.LANGFUSE_EVAL_CREATOR_WORKER_CONCURRENCY,
|
||||
duration: 2_000,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v3.109.0";
|
||||
export const VERSION = "v3.110.0";
|
||||
|
||||
@@ -21,6 +21,7 @@ import { env } from "../env";
|
||||
import { IngestionService } from "../services/IngestionService";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { ClickhouseWriter } from "../services/ClickhouseWriter";
|
||||
import { ForbiddenError } from "@langfuse/shared";
|
||||
|
||||
export const otelIngestionQueueProcessor: Processor = async (
|
||||
job: Job<TQueueJobTypes[QueueName.OtelIngestionQueue]>,
|
||||
@@ -136,6 +137,12 @@ export const otelIngestionQueueProcessor: Processor = async (
|
||||
].flat(),
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof ForbiddenError) {
|
||||
traceException(e);
|
||||
logger.warn(`Failed to parse otel observation: ${e.message}`, e);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error(
|
||||
`Failed job otel ingestion processing for ${job.data.payload.authCheck.scope.projectId}`,
|
||||
e,
|
||||
|
||||
Reference in New Issue
Block a user