chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,575 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import { workflowStateSchema } from '@/lib/api/contracts/workflows'
|
||||
import { workspacePermissionSchema } from '@/lib/api/contracts/workspaces'
|
||||
|
||||
const DEFAULT_LIMIT = 50
|
||||
const MAX_LIMIT = 250
|
||||
|
||||
const lastQueryValue = (value: unknown) => (Array.isArray(value) ? value.at(-1) : value)
|
||||
|
||||
const queryStringSchema = z.preprocess(lastQueryValue, z.string().optional())
|
||||
|
||||
const adminPaginationValueSchema = (defaultValue: number, normalize: (value: number) => number) =>
|
||||
z.preprocess(lastQueryValue, z.union([z.string(), z.number()]).optional()).transform((value) => {
|
||||
const parsed =
|
||||
typeof value === 'number'
|
||||
? value
|
||||
: typeof value === 'string'
|
||||
? Number.parseInt(value, 10)
|
||||
: Number.NaN
|
||||
|
||||
if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return normalize(parsed)
|
||||
})
|
||||
|
||||
export const adminPaginationQuerySchema = z.object({
|
||||
limit: adminPaginationValueSchema(DEFAULT_LIMIT, (limit) =>
|
||||
limit < 1 ? DEFAULT_LIMIT : Math.min(limit, MAX_LIMIT)
|
||||
),
|
||||
offset: adminPaginationValueSchema(0, (offset) => (offset < 0 ? 0 : offset)),
|
||||
})
|
||||
|
||||
export const adminIdParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const adminWorkflowVersionParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
versionId: z
|
||||
.string()
|
||||
.transform((value) => Number(value))
|
||||
.refine((value) => Number.isFinite(value) && value >= 1, {
|
||||
error: 'Invalid version number',
|
||||
}),
|
||||
})
|
||||
|
||||
export const adminWorkspaceMemberParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
memberId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const adminExportFormatQuerySchema = z.object({
|
||||
format: z.preprocess(lastQueryValue, z.enum(['zip', 'json']).catch('zip')),
|
||||
})
|
||||
|
||||
export const adminDeleteWorkspaceMemberQuerySchema = z.object({
|
||||
userId: z.preprocess(
|
||||
lastQueryValue,
|
||||
z
|
||||
.string({ error: 'userId query parameter is required' })
|
||||
.min(1, { error: 'userId query parameter is required' })
|
||||
),
|
||||
})
|
||||
|
||||
export const adminWorkspaceMemberBodySchema = z.object({
|
||||
userId: z.string({ error: 'userId is required' }).min(1, { error: 'userId is required' }),
|
||||
permissions: workspacePermissionSchema.refine((value) => value !== null, {
|
||||
error: 'permissions must be "admin", "write", or "read"',
|
||||
}),
|
||||
})
|
||||
|
||||
export const adminUpdateWorkspaceMemberBodySchema = z.object({
|
||||
permissions: workspacePermissionSchema.refine((value) => value !== null, {
|
||||
error: 'permissions must be "admin", "write", or "read"',
|
||||
}),
|
||||
})
|
||||
|
||||
export const adminExportWorkflowsBodySchema = z.object({
|
||||
ids: z
|
||||
.array(z.string(), { error: 'ids must be a non-empty array of workflow IDs' })
|
||||
.nonempty({ error: 'ids must be a non-empty array of workflow IDs' }),
|
||||
})
|
||||
|
||||
export const adminWorkflowImportBodySchema = z.object({
|
||||
workspaceId: z
|
||||
.string({ error: 'workspaceId is required' })
|
||||
.min(1, { error: 'workspaceId is required' }),
|
||||
folderId: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
workflow: z.union([
|
||||
z.string({ error: 'workflow is required' }).min(1, { error: 'workflow is required' }),
|
||||
z.record(z.string(), z.unknown()),
|
||||
]),
|
||||
})
|
||||
|
||||
export const adminWorkspaceImportQuerySchema = z.object({
|
||||
createFolders: z
|
||||
.preprocess(lastQueryValue, z.enum(['true', 'false']).catch('true'))
|
||||
.transform((value) => value !== 'false'),
|
||||
rootFolderName: queryStringSchema,
|
||||
})
|
||||
|
||||
export const adminWorkspaceImportBodySchema = z.object({
|
||||
workflows: z.array(
|
||||
z.object({
|
||||
content: z.union([z.string(), z.record(z.string(), z.unknown())]),
|
||||
name: z.string().optional(),
|
||||
folderPath: z.array(z.string()).optional(),
|
||||
}),
|
||||
{ error: 'Invalid JSON body. Expected { workflows: [...] }' }
|
||||
),
|
||||
})
|
||||
|
||||
export const adminPaginationMetaSchema = z.object({
|
||||
total: z.number(),
|
||||
limit: z.number(),
|
||||
offset: z.number(),
|
||||
hasMore: z.boolean(),
|
||||
})
|
||||
|
||||
export const adminWorkspaceSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
ownerId: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
|
||||
export const adminWorkspaceDetailSchema = adminWorkspaceSchema.extend({
|
||||
workflowCount: z.number(),
|
||||
folderCount: z.number(),
|
||||
})
|
||||
|
||||
export const adminFolderSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
parentId: z.string().nullable(),
|
||||
color: z.string().nullable(),
|
||||
sortOrder: z.number(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
|
||||
export const adminWorkflowSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
workspaceId: z.string().nullable(),
|
||||
folderId: z.string().nullable(),
|
||||
isDeployed: z.boolean(),
|
||||
deployedAt: z.string().nullable(),
|
||||
runCount: z.number(),
|
||||
lastRunAt: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
|
||||
export const adminWorkflowDetailSchema = adminWorkflowSchema.extend({
|
||||
blockCount: z.number(),
|
||||
edgeCount: z.number(),
|
||||
})
|
||||
|
||||
export const adminWorkspaceMemberSchema = z.object({
|
||||
id: z.string(),
|
||||
workspaceId: z.string(),
|
||||
userId: z.string(),
|
||||
permissions: workspacePermissionSchema,
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
userName: z.string(),
|
||||
userEmail: z.string(),
|
||||
userImage: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const adminWorkflowVariableSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.enum(['string', 'number', 'boolean', 'object', 'array', 'plain']),
|
||||
value: z.unknown(),
|
||||
})
|
||||
|
||||
export const adminWorkflowExportStateSchema = workflowStateSchema.extend({
|
||||
metadata: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
exportedAt: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
variables: z.record(z.string(), adminWorkflowVariableSchema).optional(),
|
||||
})
|
||||
|
||||
export const adminWorkflowExportPayloadSchema = z.object({
|
||||
version: z.literal('1.0'),
|
||||
exportedAt: z.string(),
|
||||
workflow: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
workspaceId: z.string().nullable(),
|
||||
folderId: z.string().nullable(),
|
||||
}),
|
||||
state: adminWorkflowExportStateSchema,
|
||||
})
|
||||
|
||||
export const adminFolderExportPayloadSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
parentId: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const adminWorkspaceExportPayloadSchema = z.object({
|
||||
version: z.literal('1.0'),
|
||||
exportedAt: z.string(),
|
||||
workspace: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
}),
|
||||
workflows: z.array(
|
||||
z.object({
|
||||
workflow: adminWorkflowExportPayloadSchema.shape.workflow,
|
||||
state: adminWorkflowExportStateSchema,
|
||||
})
|
||||
),
|
||||
folders: z.array(adminFolderExportPayloadSchema),
|
||||
})
|
||||
|
||||
export const adminFolderFullExportPayloadSchema = z.object({
|
||||
version: z.literal('1.0'),
|
||||
exportedAt: z.string(),
|
||||
folder: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
}),
|
||||
workflows: z.array(
|
||||
z.object({
|
||||
workflow: adminWorkflowExportPayloadSchema.shape.workflow.omit({ workspaceId: true }),
|
||||
state: adminWorkflowExportStateSchema,
|
||||
})
|
||||
),
|
||||
folders: z.array(adminFolderExportPayloadSchema),
|
||||
})
|
||||
|
||||
export const adminImportResultSchema = z.object({
|
||||
workflowId: z.string(),
|
||||
name: z.string(),
|
||||
success: z.boolean(),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
|
||||
export const adminWorkflowImportResponseSchema = z.object({
|
||||
workflowId: z.string(),
|
||||
name: z.string(),
|
||||
success: z.literal(true),
|
||||
})
|
||||
|
||||
export const adminWorkspaceImportResponseSchema = z.object({
|
||||
imported: z.number(),
|
||||
failed: z.number(),
|
||||
results: z.array(adminImportResultSchema),
|
||||
})
|
||||
|
||||
export const adminDeploymentVersionSchema = z.object({
|
||||
id: z.string(),
|
||||
version: z.number(),
|
||||
name: z.string().nullable(),
|
||||
isActive: z.boolean(),
|
||||
createdAt: z.string(),
|
||||
createdBy: z.string().nullable(),
|
||||
deployedByName: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const adminDeployResultSchema = z.object({
|
||||
isDeployed: z.literal(true),
|
||||
version: z.number(),
|
||||
deployedAt: z.string(),
|
||||
warnings: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export const adminUndeployResultSchema = z.object({
|
||||
isDeployed: z.literal(false),
|
||||
})
|
||||
|
||||
const adminSingleResponseSchema = <TSchema extends z.ZodType>(schema: TSchema) =>
|
||||
z.object({ data: schema })
|
||||
|
||||
const adminListResponseSchema = <TSchema extends z.ZodType>(schema: TSchema) =>
|
||||
z.object({
|
||||
data: z.array(schema),
|
||||
pagination: adminPaginationMetaSchema,
|
||||
})
|
||||
|
||||
export const adminListWorkflowsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/v1/admin/workflows',
|
||||
query: adminPaginationQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminListResponseSchema(adminWorkflowSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminGetWorkflowContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/v1/admin/workflows/[id]',
|
||||
params: adminIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminSingleResponseSchema(adminWorkflowDetailSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminDeleteWorkflowContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/v1/admin/workflows/[id]',
|
||||
params: adminIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
workflowId: z.string(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminDeployWorkflowContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/v1/admin/workflows/[id]/deploy',
|
||||
params: adminIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminSingleResponseSchema(adminDeployResultSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminUndeployWorkflowContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/v1/admin/workflows/[id]/deploy',
|
||||
params: adminIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminSingleResponseSchema(adminUndeployResultSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminListWorkflowVersionsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/v1/admin/workflows/[id]/versions',
|
||||
params: adminIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminSingleResponseSchema(
|
||||
z.object({
|
||||
versions: z.array(adminDeploymentVersionSchema),
|
||||
})
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminActivateWorkflowVersionContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/v1/admin/workflows/[id]/versions/[versionId]/activate',
|
||||
params: adminWorkflowVersionParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminSingleResponseSchema(
|
||||
z.object({
|
||||
success: z.literal(true),
|
||||
version: z.number(),
|
||||
deployedAt: z.string(),
|
||||
warnings: z.array(z.string()).optional(),
|
||||
})
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminExportWorkflowContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/v1/admin/workflows/[id]/export',
|
||||
params: adminIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminSingleResponseSchema(adminWorkflowExportPayloadSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminExportWorkflowsContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/v1/admin/workflows/export',
|
||||
query: adminExportFormatQuerySchema,
|
||||
body: adminExportWorkflowsBodySchema,
|
||||
response: {
|
||||
mode: 'binary',
|
||||
},
|
||||
})
|
||||
|
||||
export const adminImportWorkflowContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/v1/admin/workflows/import',
|
||||
body: adminWorkflowImportBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminWorkflowImportResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const adminListWorkspacesContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/v1/admin/workspaces',
|
||||
query: adminPaginationQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminListResponseSchema(adminWorkspaceSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminGetWorkspaceContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/v1/admin/workspaces/[id]',
|
||||
params: adminIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminSingleResponseSchema(adminWorkspaceDetailSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminListWorkspaceWorkflowsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/v1/admin/workspaces/[id]/workflows',
|
||||
params: adminIdParamsSchema,
|
||||
query: adminPaginationQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminListResponseSchema(adminWorkflowSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminDeleteWorkspaceWorkflowsContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/v1/admin/workspaces/[id]/workflows',
|
||||
params: adminIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
deleted: z.number(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminListWorkspaceFoldersContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/v1/admin/workspaces/[id]/folders',
|
||||
params: adminIdParamsSchema,
|
||||
query: adminPaginationQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminListResponseSchema(adminFolderSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminExportWorkspaceContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/v1/admin/workspaces/[id]/export',
|
||||
params: adminIdParamsSchema,
|
||||
query: adminExportFormatQuerySchema,
|
||||
response: {
|
||||
mode: 'binary',
|
||||
},
|
||||
})
|
||||
|
||||
export const adminImportWorkspaceContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/v1/admin/workspaces/[id]/import',
|
||||
params: adminIdParamsSchema,
|
||||
query: adminWorkspaceImportQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminWorkspaceImportResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const adminListWorkspaceMembersContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/v1/admin/workspaces/[id]/members',
|
||||
params: adminIdParamsSchema,
|
||||
query: adminPaginationQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminListResponseSchema(adminWorkspaceMemberSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminCreateWorkspaceMemberContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/v1/admin/workspaces/[id]/members',
|
||||
params: adminIdParamsSchema,
|
||||
body: adminWorkspaceMemberBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminSingleResponseSchema(
|
||||
adminWorkspaceMemberSchema.extend({
|
||||
action: z.enum(['created', 'updated', 'already_member']),
|
||||
})
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminDeleteWorkspaceMemberContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/v1/admin/workspaces/[id]/members',
|
||||
params: adminIdParamsSchema,
|
||||
query: adminDeleteWorkspaceMemberQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminSingleResponseSchema(
|
||||
z.object({
|
||||
removed: z.literal(true),
|
||||
userId: z.string(),
|
||||
workspaceId: z.string(),
|
||||
})
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminGetWorkspaceMemberContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/v1/admin/workspaces/[id]/members/[memberId]',
|
||||
params: adminWorkspaceMemberParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminSingleResponseSchema(adminWorkspaceMemberSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminUpdateWorkspaceMemberContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/v1/admin/workspaces/[id]/members/[memberId]',
|
||||
params: adminWorkspaceMemberParamsSchema,
|
||||
body: adminUpdateWorkspaceMemberBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminSingleResponseSchema(adminWorkspaceMemberSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminRemoveWorkspaceMemberContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/v1/admin/workspaces/[id]/members/[memberId]',
|
||||
params: adminWorkspaceMemberParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: adminSingleResponseSchema(
|
||||
z.object({
|
||||
removed: z.literal(true),
|
||||
memberId: z.string(),
|
||||
userId: z.string(),
|
||||
workspaceId: z.string(),
|
||||
})
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
export const adminExportFolderContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/v1/admin/folders/[id]/export',
|
||||
params: adminIdParamsSchema,
|
||||
query: adminExportFormatQuerySchema,
|
||||
response: {
|
||||
mode: 'binary',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,142 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const apiKeySchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
key: z.string(),
|
||||
displayKey: z.string().optional(),
|
||||
lastUsed: z.string().nullable().optional(),
|
||||
createdAt: z.string(),
|
||||
expiresAt: z.string().nullable().optional(),
|
||||
createdBy: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
export type ApiKey = z.output<typeof apiKeySchema>
|
||||
|
||||
export const createApiKeyBodySchema = z.object({
|
||||
name: z.string().trim().min(1, 'Name is required'),
|
||||
source: z.enum(['settings', 'deploy_modal']).optional(),
|
||||
})
|
||||
|
||||
export const createPersonalApiKeyBodySchema = createApiKeyBodySchema.pick({ name: true })
|
||||
|
||||
export const apiKeyIdParamsSchema = z.object({
|
||||
id: z.string({ error: 'API key ID is required' }).min(1, 'API key ID is required'),
|
||||
})
|
||||
|
||||
export const workspaceApiKeyParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const workspaceApiKeyIdParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
keyId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const updateWorkspaceApiKeyBodySchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
})
|
||||
|
||||
export const deleteWorkspaceApiKeysBodySchema = z.object({
|
||||
keys: z.array(z.string()).min(1),
|
||||
})
|
||||
|
||||
export const listPersonalApiKeysContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/users/me/api-keys',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
keys: z.array(apiKeySchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const createPersonalApiKeyContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/users/me/api-keys',
|
||||
body: createPersonalApiKeyBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
key: apiKeySchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const deletePersonalApiKeyContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/users/me/api-keys/[id]',
|
||||
params: apiKeyIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const listWorkspaceApiKeysContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/workspaces/[id]/api-keys',
|
||||
params: workspaceApiKeyParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
keys: z.array(apiKeySchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const createWorkspaceApiKeyContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/workspaces/[id]/api-keys',
|
||||
params: workspaceApiKeyParamsSchema,
|
||||
body: createApiKeyBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
key: apiKeySchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteWorkspaceApiKeyContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/workspaces/[id]/api-keys/[keyId]',
|
||||
params: workspaceApiKeyIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateWorkspaceApiKeyContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/workspaces/[id]/api-keys/[keyId]',
|
||||
params: workspaceApiKeyIdParamsSchema,
|
||||
body: updateWorkspaceApiKeyBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
key: apiKeySchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteWorkspaceApiKeysContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/workspaces/[id]/api-keys',
|
||||
params: workspaceApiKeyParamsSchema,
|
||||
body: deleteWorkspaceApiKeysBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
deletedCount: z.number(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,87 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const auditLogsQuerySchema = z.object({
|
||||
search: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) => value?.trim() || undefined),
|
||||
action: z.string().optional(),
|
||||
resourceType: z.string().optional(),
|
||||
actorId: z.string().optional(),
|
||||
startDate: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((value) => !value || !Number.isNaN(Date.parse(value)), {
|
||||
message: 'Invalid startDate format',
|
||||
}),
|
||||
endDate: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((value) => !value || !Number.isNaN(Date.parse(value)), {
|
||||
message: 'Invalid endDate format',
|
||||
}),
|
||||
includeDeparted: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) => value === 'true'),
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) => Math.min(Math.max(Number(value) || 50, 1), 100)),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
export type AuditLogsQuery = z.output<typeof auditLogsQuerySchema>
|
||||
|
||||
export const enterpriseAuditLogEntrySchema = z.object({
|
||||
id: z.string(),
|
||||
workspaceId: z.string().nullable(),
|
||||
actorId: z.string().nullable(),
|
||||
actorName: z.string().nullable(),
|
||||
actorEmail: z.string().nullable(),
|
||||
action: z.string(),
|
||||
resourceType: z.string(),
|
||||
resourceId: z.string().nullable(),
|
||||
resourceName: z.string().nullable(),
|
||||
description: z.string().nullable(),
|
||||
metadata: z.unknown(),
|
||||
createdAt: z.string(),
|
||||
})
|
||||
|
||||
export type EnterpriseAuditLogEntry = z.output<typeof enterpriseAuditLogEntrySchema>
|
||||
|
||||
export const listAuditLogsResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
data: z.array(enterpriseAuditLogEntrySchema),
|
||||
nextCursor: z.string().optional(),
|
||||
})
|
||||
|
||||
export type AuditLogPage = z.output<typeof listAuditLogsResponseSchema>
|
||||
|
||||
export const listAuditLogsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/audit-logs',
|
||||
query: auditLogsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: listAuditLogsResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const exportAuditLogsQuerySchema = auditLogsQuerySchema.omit({ limit: true, cursor: true })
|
||||
export type ExportAuditLogsQuery = z.output<typeof exportAuditLogsQuerySchema>
|
||||
|
||||
/**
|
||||
* CSV download of every audit log matching the filter (no pagination). `mode:
|
||||
* 'text'` because a CSV response has no JSON schema to validate; the client
|
||||
* triggers this via `fetch` + blob (not `requestJson`), so there's no
|
||||
* response shape for a consumer to type.
|
||||
*/
|
||||
export const exportAuditLogsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/audit-logs/export',
|
||||
query: exportAuditLogsQuerySchema,
|
||||
response: {
|
||||
mode: 'text',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,127 @@
|
||||
import { z } from 'zod'
|
||||
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const ssoProvidersQuerySchema = z.object({
|
||||
organizationId: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
export const authProviderStatusResponseSchema = z.object({
|
||||
githubAvailable: z.boolean(),
|
||||
googleAvailable: z.boolean(),
|
||||
microsoftAvailable: z.boolean(),
|
||||
registrationDisabled: z.boolean(),
|
||||
})
|
||||
|
||||
const ssoMappingSchema = z
|
||||
.object({
|
||||
id: z.string().default('sub'),
|
||||
email: z.string().default('email'),
|
||||
name: z.string().default('name'),
|
||||
image: z.string().default('picture'),
|
||||
})
|
||||
.default({
|
||||
id: 'sub',
|
||||
email: 'email',
|
||||
name: 'name',
|
||||
image: 'picture',
|
||||
})
|
||||
|
||||
export const ssoRegistrationBodySchema = z.discriminatedUnion('providerType', [
|
||||
z.object({
|
||||
providerType: z.literal('oidc').default('oidc'),
|
||||
providerId: z.string().min(1, 'Provider ID is required'),
|
||||
issuer: z.string().url('Issuer must be a valid URL'),
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
orgId: z.string().optional(),
|
||||
mapping: ssoMappingSchema,
|
||||
clientId: z.string().min(1, 'Client ID is required for OIDC'),
|
||||
clientSecret: z.string().min(1, 'Client Secret is required for OIDC'),
|
||||
scopes: z
|
||||
.union([
|
||||
z.string().transform((s) =>
|
||||
s
|
||||
.split(',')
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value !== '')
|
||||
),
|
||||
z.array(z.string()),
|
||||
])
|
||||
.default(['openid', 'profile', 'email']),
|
||||
pkce: z.boolean().default(true),
|
||||
authorizationEndpoint: z.string().url().optional(),
|
||||
tokenEndpoint: z.string().url().optional(),
|
||||
userInfoEndpoint: z.string().url().optional(),
|
||||
skipUserInfoEndpoint: z.boolean().default(false),
|
||||
jwksEndpoint: z.string().url().optional(),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal('saml'),
|
||||
providerId: z.string().min(1, 'Provider ID is required'),
|
||||
issuer: z.string().url('Issuer must be a valid URL'),
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
orgId: z.string().optional(),
|
||||
mapping: ssoMappingSchema,
|
||||
entryPoint: z.string().url('Entry point must be a valid URL for SAML'),
|
||||
cert: z.string().min(1, 'Certificate is required for SAML'),
|
||||
callbackUrl: z.string().url().optional(),
|
||||
audience: z.string().optional(),
|
||||
wantAssertionsSigned: z.boolean().optional(),
|
||||
signatureAlgorithm: z.string().optional(),
|
||||
digestAlgorithm: z.string().optional(),
|
||||
identifierFormat: z.string().optional(),
|
||||
idpMetadata: z.string().optional(),
|
||||
}),
|
||||
])
|
||||
|
||||
export type SsoRegistrationBody = z.input<typeof ssoRegistrationBodySchema>
|
||||
|
||||
export const ssoRegistrationContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/auth/sso/register',
|
||||
body: ssoRegistrationBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
providerId: z.string(),
|
||||
providerType: z.enum(['oidc', 'saml']),
|
||||
message: z.string(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const ssoProviderListEntrySchema = z.object({
|
||||
id: z.string().optional(),
|
||||
providerId: z.string().optional(),
|
||||
domain: z.string().nullable(),
|
||||
issuer: z.string().nullable().optional(),
|
||||
oidcConfig: z.string().nullable().optional(),
|
||||
samlConfig: z.string().nullable().optional(),
|
||||
userId: z.string().nullable().optional(),
|
||||
organizationId: z.string().nullable().optional(),
|
||||
providerType: z.enum(['oidc', 'saml']).optional(),
|
||||
})
|
||||
|
||||
export const listSsoProvidersContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/auth/sso/providers',
|
||||
query: ssoProvidersQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
providers: z.array(ssoProviderListEntrySchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const getAuthProvidersContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/auth/providers',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: authProviderStatusResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export type AuthProviderStatusResponse = ContractJsonResponse<typeof getAuthProvidersContract>
|
||||
@@ -0,0 +1,37 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts'
|
||||
import { workspaceIdSchema } from '@/lib/api/contracts/primitives'
|
||||
|
||||
/**
|
||||
* Per-viewer block visibility projection (see
|
||||
* `@/lib/core/config/block-visibility`): which `preview: true` block types this
|
||||
* viewer may see, which types are kill-switched, and which revealed types carry
|
||||
* the " (Preview)" display tag.
|
||||
*/
|
||||
|
||||
const getBlockVisibilityQuerySchema = z.object({
|
||||
workspaceId: workspaceIdSchema,
|
||||
})
|
||||
|
||||
export type GetBlockVisibilityQuery = z.input<typeof getBlockVisibilityQuerySchema>
|
||||
|
||||
const blockVisibilityResponseSchema = z.object({
|
||||
/** Preview block types revealed to this viewer. */
|
||||
revealed: z.array(z.string()),
|
||||
/** Block types kill-switched (hidden from discovery) for this viewer. */
|
||||
disabled: z.array(z.string()),
|
||||
/** Revealed types not globally GA — displayed with a " (Preview)" suffix. */
|
||||
previewTagged: z.array(z.string()),
|
||||
})
|
||||
|
||||
export type BlockVisibilityResponse = z.output<typeof blockVisibilityResponseSchema>
|
||||
|
||||
export const getBlockVisibilityContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/blocks/visibility',
|
||||
query: getBlockVisibilityQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: blockVisibilityResponseSchema,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,124 @@
|
||||
import { z } from 'zod'
|
||||
import { type ContractJsonResponse, defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const byokProviderIdSchema = z.enum([
|
||||
'openai',
|
||||
'anthropic',
|
||||
'google',
|
||||
'mistral',
|
||||
'zai',
|
||||
'xai',
|
||||
'fireworks',
|
||||
'together',
|
||||
'baseten',
|
||||
'ollama-cloud',
|
||||
'falai',
|
||||
'firecrawl',
|
||||
'exa',
|
||||
'context_dev',
|
||||
'serper',
|
||||
'linkup',
|
||||
'perplexity',
|
||||
'jina',
|
||||
'google_cloud',
|
||||
'parallel_ai',
|
||||
'brandfetch',
|
||||
'cohere',
|
||||
'hunter',
|
||||
'peopledatalabs',
|
||||
'findymail',
|
||||
'prospeo',
|
||||
'wiza',
|
||||
'zerobounce',
|
||||
'neverbounce',
|
||||
'millionverifier',
|
||||
'datagma',
|
||||
'dropcontact',
|
||||
'leadmagic',
|
||||
'icypeas',
|
||||
'enrow',
|
||||
])
|
||||
|
||||
/** Maximum number of keys a workspace may store per provider. */
|
||||
export const MAX_BYOK_KEYS_PER_PROVIDER = 10
|
||||
|
||||
export const byokKeySchema = z.object({
|
||||
id: z.string(),
|
||||
providerId: byokProviderIdSchema,
|
||||
name: z.string().nullable(),
|
||||
maskedKey: z.string(),
|
||||
createdBy: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
|
||||
export type BYOKKey = z.output<typeof byokKeySchema>
|
||||
|
||||
export const byokKeyMutationSchema = z.object({
|
||||
id: z.string(),
|
||||
providerId: byokProviderIdSchema,
|
||||
name: z.string().nullable(),
|
||||
maskedKey: z.string(),
|
||||
createdAt: z.string().optional(),
|
||||
updatedAt: z.string().optional(),
|
||||
})
|
||||
|
||||
export const byokWorkspaceParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const upsertByokKeyBodySchema = z.object({
|
||||
providerId: byokProviderIdSchema,
|
||||
apiKey: z.string().min(1, 'API key is required'),
|
||||
/** When set, updates that specific key; otherwise a new key is added for the provider. */
|
||||
keyId: z.string().min(1, 'keyId cannot be empty').optional(),
|
||||
/** Display label for the key. An empty string clears the label. */
|
||||
name: z.string().trim().max(120, 'Name must be 120 characters or fewer').optional(),
|
||||
})
|
||||
|
||||
export const deleteByokKeyBodySchema = z.object({
|
||||
providerId: byokProviderIdSchema,
|
||||
/** When set, deletes only that key; otherwise every key for the provider is removed. */
|
||||
keyId: z.string().min(1, 'keyId cannot be empty').optional(),
|
||||
})
|
||||
|
||||
export const listByokKeysContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/workspaces/[id]/byok-keys',
|
||||
params: byokWorkspaceParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
keys: z.array(byokKeySchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const upsertByokKeyContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/workspaces/[id]/byok-keys',
|
||||
params: byokWorkspaceParamsSchema,
|
||||
body: upsertByokKeyBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
key: byokKeyMutationSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteByokKeyContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/workspaces/[id]/byok-keys',
|
||||
params: byokWorkspaceParamsSchema,
|
||||
body: deleteByokKeyBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export type BYOKKeysResponse = ContractJsonResponse<typeof listByokKeysContract>
|
||||
@@ -0,0 +1,279 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const chatAuthTypeSchema = z.enum(['public', 'password', 'email', 'sso'])
|
||||
export type ChatAuthType = z.output<typeof chatAuthTypeSchema>
|
||||
|
||||
export const chatIdParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const chatIdentifierParamsSchema = z.object({
|
||||
identifier: z.string().min(1),
|
||||
})
|
||||
|
||||
export const chatOutputConfigSchema = z.object({
|
||||
blockId: z.string().min(1),
|
||||
path: z.string().min(1),
|
||||
})
|
||||
|
||||
export const deployedChatOutputConfigSchema = z.object({
|
||||
blockId: z.string(),
|
||||
path: z.string().optional(),
|
||||
})
|
||||
|
||||
export const chatCustomizationsSchema = z.object({
|
||||
primaryColor: z.string(),
|
||||
welcomeMessage: z.string(),
|
||||
imageUrl: z.string().optional(),
|
||||
})
|
||||
|
||||
export const createChatBodySchema = z.object({
|
||||
workflowId: z.string().min(1, 'Workflow ID is required'),
|
||||
identifier: z
|
||||
.string()
|
||||
.min(1, 'Identifier is required')
|
||||
.regex(/^[a-z0-9-]+$/, 'Identifier can only contain lowercase letters, numbers, and hyphens'),
|
||||
title: z.string().min(1, 'Title is required'),
|
||||
description: z.string().optional(),
|
||||
customizations: chatCustomizationsSchema,
|
||||
authType: chatAuthTypeSchema.default('public'),
|
||||
password: z.string().optional(),
|
||||
allowedEmails: z.array(z.string()).optional().default([]),
|
||||
outputConfigs: z.array(chatOutputConfigSchema).optional().default([]),
|
||||
})
|
||||
export type CreateChatBody = z.input<typeof createChatBodySchema>
|
||||
|
||||
export const updateChatBodySchema = z.object({
|
||||
workflowId: z.string().min(1, 'Workflow ID is required').optional(),
|
||||
identifier: z
|
||||
.string()
|
||||
.min(1, 'Identifier is required')
|
||||
.regex(/^[a-z0-9-]+$/, 'Identifier can only contain lowercase letters, numbers, and hyphens')
|
||||
.optional(),
|
||||
title: z.string().min(1, 'Title is required').optional(),
|
||||
description: z.string().optional(),
|
||||
customizations: chatCustomizationsSchema.optional(),
|
||||
authType: chatAuthTypeSchema.optional(),
|
||||
password: z.string().optional(),
|
||||
allowedEmails: z.array(z.string()).optional(),
|
||||
outputConfigs: z.array(chatOutputConfigSchema).optional(),
|
||||
})
|
||||
export type UpdateChatBody = z.input<typeof updateChatBodySchema>
|
||||
|
||||
export const createChatResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
chatId: z.string(),
|
||||
chatUrl: z.string(),
|
||||
message: z.string(),
|
||||
})
|
||||
export type CreateChatResponse = z.output<typeof createChatResponseSchema>
|
||||
|
||||
export const updateChatResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
chatUrl: z.string(),
|
||||
message: z.string(),
|
||||
})
|
||||
export type UpdateChatResponse = z.output<typeof updateChatResponseSchema>
|
||||
|
||||
export const deleteChatResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
})
|
||||
|
||||
export const deployedChatConfigSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
description: z.preprocess((value) => value ?? '', z.string()),
|
||||
customizations: z.preprocess(
|
||||
(value) => value ?? {},
|
||||
z
|
||||
.object({
|
||||
primaryColor: z.string().optional(),
|
||||
logoUrl: z.string().optional(),
|
||||
imageUrl: z.string().optional(),
|
||||
welcomeMessage: z.string().optional(),
|
||||
headerText: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
),
|
||||
authType: z.preprocess((value) => value ?? 'public', chatAuthTypeSchema),
|
||||
outputConfigs: z.preprocess(
|
||||
(value) => value ?? undefined,
|
||||
z.array(deployedChatOutputConfigSchema).optional()
|
||||
),
|
||||
})
|
||||
export type DeployedChatConfig = z.output<typeof deployedChatConfigSchema>
|
||||
|
||||
export const deployedChatAuthBodySchema = z.object({
|
||||
password: z.string().max(1024, 'Password is too long').optional(),
|
||||
email: z.string().email('Invalid email format').optional().or(z.literal('')),
|
||||
})
|
||||
export type DeployedChatAuthBody = z.input<typeof deployedChatAuthBodySchema>
|
||||
|
||||
const MAX_CHAT_INPUT_CHARS = 1_000_000
|
||||
const MAX_CHAT_FILE_DATA_CHARS = 14 * 1024 * 1024
|
||||
const MAX_CHAT_FILES = 15
|
||||
|
||||
export const deployedChatFileSchema = z.object({
|
||||
name: z.string().min(1, 'File name is required').max(255, 'File name is too long'),
|
||||
type: z.string().min(1, 'File type is required').max(255, 'File type is too long'),
|
||||
size: z.number().positive('File size must be positive'),
|
||||
data: z
|
||||
.string()
|
||||
.min(1, 'File data is required')
|
||||
.max(MAX_CHAT_FILE_DATA_CHARS, 'File data exceeds the maximum allowed size'),
|
||||
lastModified: z.number().optional(),
|
||||
})
|
||||
|
||||
export const deployedChatPostBodySchema = z.object({
|
||||
input: z.string().max(MAX_CHAT_INPUT_CHARS, 'Input is too long').optional(),
|
||||
password: z.string().max(1024, 'Password is too long').optional(),
|
||||
email: z.string().email('Invalid email format').optional().or(z.literal('')),
|
||||
conversationId: z.string().max(256, 'Conversation ID is too long').optional(),
|
||||
files: z
|
||||
.array(deployedChatFileSchema)
|
||||
.max(MAX_CHAT_FILES, `A maximum of ${MAX_CHAT_FILES} files is allowed`)
|
||||
.optional()
|
||||
.default([]),
|
||||
})
|
||||
export type DeployedChatPostBody = z.input<typeof deployedChatPostBodySchema>
|
||||
|
||||
export const chatSSOBodySchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
})
|
||||
|
||||
export const chatSSOResponseSchema = z.object({
|
||||
eligible: z.boolean(),
|
||||
})
|
||||
export type ChatSSOResponse = z.output<typeof chatSSOResponseSchema>
|
||||
|
||||
export const chatEmailOtpRequestBodySchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
})
|
||||
|
||||
export const chatEmailOtpVerifyBodySchema = chatEmailOtpRequestBodySchema.extend({
|
||||
otp: z.string().length(6, 'OTP must be 6 digits'),
|
||||
})
|
||||
|
||||
export const chatEmailOtpRequestResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
})
|
||||
|
||||
export const identifierValidationQuerySchema = z.object({
|
||||
identifier: z
|
||||
.string()
|
||||
.min(1, 'Identifier is required')
|
||||
.regex(/^[a-z0-9-]+$/, 'Identifier can only contain lowercase letters, numbers, and hyphens')
|
||||
.max(100, 'Identifier must be 100 characters or less'),
|
||||
})
|
||||
|
||||
export const identifierValidationResponseSchema = z.object({
|
||||
available: z.boolean(),
|
||||
error: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
export const createChatContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/chat',
|
||||
body: createChatBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: createChatResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getDeployedChatConfigContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/chat/[identifier]',
|
||||
params: chatIdentifierParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: deployedChatConfigSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const authenticateDeployedChatContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/chat/[identifier]',
|
||||
params: chatIdentifierParamsSchema,
|
||||
body: deployedChatAuthBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: deployedChatConfigSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const deployedChatPostContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/chat/[identifier]',
|
||||
params: chatIdentifierParamsSchema,
|
||||
body: deployedChatPostBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: deployedChatConfigSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const chatSSOContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/chat/[identifier]/sso',
|
||||
params: chatIdentifierParamsSchema,
|
||||
body: chatSSOBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: chatSSOResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const requestChatEmailOtpContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/chat/[identifier]/otp',
|
||||
params: chatIdentifierParamsSchema,
|
||||
body: chatEmailOtpRequestBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: chatEmailOtpRequestResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const verifyChatEmailOtpContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/chat/[identifier]/otp',
|
||||
params: chatIdentifierParamsSchema,
|
||||
body: chatEmailOtpVerifyBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: deployedChatConfigSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const validateChatIdentifierContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/chat/validate',
|
||||
query: identifierValidationQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: identifierValidationResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateChatContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/chat/manage/[id]',
|
||||
params: chatIdParamsSchema,
|
||||
body: updateChatBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: updateChatResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteChatContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/chat/manage/[id]',
|
||||
params: chatIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: deleteChatResponseSchema,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,130 @@
|
||||
import { z } from 'zod'
|
||||
import { jobIdParamsSchema } from '@/lib/api/contracts/primitives'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const NO_EMAIL_HEADER_CONTROL_CHARS_REGEX = /^[^\r\n\u0000-\u001F\u007F]+$/
|
||||
|
||||
export const helpFormBodySchema = z.object({
|
||||
subject: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Subject is required')
|
||||
.regex(NO_EMAIL_HEADER_CONTROL_CHARS_REGEX, 'Invalid characters'),
|
||||
message: z.string().min(1, 'Message is required'),
|
||||
type: z.enum(['bug', 'feedback', 'feature_request', 'other']),
|
||||
})
|
||||
export type HelpFormBody = z.input<typeof helpFormBodySchema>
|
||||
|
||||
export const emailPreviewQuerySchema = z.object({
|
||||
template: z.string().optional(),
|
||||
})
|
||||
|
||||
export const integrationRequestBodySchema = z.object({
|
||||
integrationName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Integration name is required')
|
||||
.max(200)
|
||||
.regex(NO_EMAIL_HEADER_CONTROL_CHARS_REGEX, 'Invalid characters'),
|
||||
email: z.string().email('A valid email is required'),
|
||||
useCase: z.string().max(2000).optional(),
|
||||
})
|
||||
export type IntegrationRequestBody = z.input<typeof integrationRequestBodySchema>
|
||||
|
||||
export const integrationRequestResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
message: z.string(),
|
||||
})
|
||||
|
||||
export const integrationRequestContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/help/integration-request',
|
||||
body: integrationRequestBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: integrationRequestResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getAllowedProvidersContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/settings/allowed-providers',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
blacklistedProviders: z.array(z.string()),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const getAllowedIntegrationsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/settings/allowed-integrations',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
// `null` means "no env-derived allowlist" (unrestricted); a non-null
|
||||
// array narrows the visible integrations.
|
||||
allowedIntegrations: z.array(z.string()).nullable(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const getVoiceSettingsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/settings/voice',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
sttAvailable: z.boolean(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const getStarsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/stars',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
stars: z.string(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const getStatusContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/status',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
status: z.enum(['operational', 'degraded', 'outage', 'maintenance', 'loading', 'error']),
|
||||
message: z.string(),
|
||||
url: z.string().url(),
|
||||
lastUpdated: z.string(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const jobStatusSchema = z.enum(['pending', 'processing', 'completed', 'failed'])
|
||||
|
||||
const jobStatusResponseSchema = z
|
||||
.object({
|
||||
success: z.literal(true),
|
||||
taskId: z.string(),
|
||||
status: jobStatusSchema,
|
||||
metadata: z.record(z.string(), z.unknown()).nullable().optional(),
|
||||
output: z.unknown().optional(),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const getJobStatusContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/jobs/[jobId]',
|
||||
params: jobIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: jobStatusResponseSchema,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,106 @@
|
||||
import { z } from 'zod'
|
||||
import type { ContractBodyInput } from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import { NO_EMAIL_HEADER_CONTROL_CHARS_REGEX } from '@/lib/messaging/email/utils'
|
||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||
|
||||
export const CONTACT_TOPIC_VALUES = [
|
||||
'general',
|
||||
'support',
|
||||
'integration',
|
||||
'feature_request',
|
||||
'sales',
|
||||
'partnership',
|
||||
'billing',
|
||||
'other',
|
||||
] as const
|
||||
|
||||
export const CONTACT_TOPIC_OPTIONS = [
|
||||
{ value: 'general', label: 'General question' },
|
||||
{ value: 'support', label: 'Technical support' },
|
||||
{ value: 'integration', label: 'Integration request' },
|
||||
{ value: 'feature_request', label: 'Feature request' },
|
||||
{ value: 'sales', label: 'Sales & pricing' },
|
||||
{ value: 'partnership', label: 'Partnership' },
|
||||
{ value: 'billing', label: 'Billing' },
|
||||
{ value: 'other', label: 'Other' },
|
||||
] as const
|
||||
|
||||
export const contactRequestSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Name is required')
|
||||
.max(120, 'Name must be 120 characters or less')
|
||||
.regex(NO_EMAIL_HEADER_CONTROL_CHARS_REGEX, 'Invalid characters'),
|
||||
email: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Email is required')
|
||||
.max(320)
|
||||
.transform((value) => value.toLowerCase())
|
||||
.refine((value) => quickValidateEmail(value).isValid, 'Enter a valid email'),
|
||||
company: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(120, 'Company must be 120 characters or less')
|
||||
.optional()
|
||||
.transform((value) => (value && value.length > 0 ? value : undefined)),
|
||||
topic: z.enum(CONTACT_TOPIC_VALUES, {
|
||||
error: 'Please select a topic',
|
||||
}),
|
||||
subject: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Subject is required')
|
||||
.max(200, 'Subject must be 200 characters or less')
|
||||
.regex(NO_EMAIL_HEADER_CONTROL_CHARS_REGEX, 'Invalid characters'),
|
||||
message: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Message is required')
|
||||
.max(5000, 'Message must be 5,000 characters or less'),
|
||||
})
|
||||
|
||||
export type ContactRequestPayload = z.infer<typeof contactRequestSchema>
|
||||
export type ContactRequestBody = z.input<typeof contactRequestSchema>
|
||||
|
||||
export const submitContactBodySchema = contactRequestSchema.extend({
|
||||
website: z.string().optional(),
|
||||
captchaToken: z.string().optional(),
|
||||
})
|
||||
|
||||
export function getContactTopicLabel(value: ContactRequestPayload['topic']): string {
|
||||
return CONTACT_TOPIC_OPTIONS.find((option) => option.value === value)?.label ?? value
|
||||
}
|
||||
|
||||
export type HelpEmailType = 'bug' | 'feedback' | 'feature_request' | 'other'
|
||||
|
||||
/**
|
||||
* Map a contact topic to the confirmation-email type. Only `feature_request` has
|
||||
* a matching email label ("Feature Request"); every other topic — support, sales,
|
||||
* billing, etc. — resolves to `other` ("General Inquiry") so the confirmation copy
|
||||
* never mislabels the request (e.g. calling a support inquiry a "bug report").
|
||||
*/
|
||||
export function mapContactTopicToHelpType(topic: ContactRequestPayload['topic']): HelpEmailType {
|
||||
return topic === 'feature_request' ? 'feature_request' : 'other'
|
||||
}
|
||||
|
||||
export const contactResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
message: z.string(),
|
||||
})
|
||||
|
||||
export type SubmitContactResult = z.output<typeof contactResponseSchema>
|
||||
|
||||
export const submitContactContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/contact',
|
||||
body: submitContactBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: contactResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export type SubmitContactBody = ContractBodyInput<typeof submitContactContract>
|
||||
@@ -0,0 +1,775 @@
|
||||
import { z } from 'zod'
|
||||
import { type ContractJsonResponse, defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import { cleanedWorkflowStateSchema } from '@/lib/api/contracts/workflows'
|
||||
import {
|
||||
ASYNC_TOOL_CONFIRMATION_STATUS,
|
||||
type AsyncConfirmationStatus,
|
||||
} from '@/lib/copilot/async-runs/lifecycle'
|
||||
|
||||
export const copilotApiKeySchema = z.object({
|
||||
id: z.string(),
|
||||
displayKey: z.string(),
|
||||
name: z.string().nullable(),
|
||||
createdAt: z.string().nullable(),
|
||||
lastUsed: z.string().nullable(),
|
||||
})
|
||||
|
||||
export type CopilotApiKey = z.output<typeof copilotApiKeySchema>
|
||||
|
||||
export const deleteCopilotApiKeyQuerySchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const generateCopilotApiKeyBodySchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').max(255, 'Name is too long'),
|
||||
})
|
||||
|
||||
export const submitCopilotFeedbackBodySchema = z.object({
|
||||
chatId: z.string().uuid('Chat ID must be a valid UUID'),
|
||||
userQuery: z.string().min(1, 'User query is required'),
|
||||
agentResponse: z.string().min(1, 'Agent response is required'),
|
||||
isPositiveFeedback: z.boolean(),
|
||||
feedback: z.string().optional(),
|
||||
workflowYaml: z.string().optional(),
|
||||
})
|
||||
|
||||
export type SubmitCopilotFeedbackBody = z.input<typeof submitCopilotFeedbackBodySchema>
|
||||
|
||||
export const copilotCredentialsQuerySchema = z.object({})
|
||||
|
||||
export const copilotConfirmBodySchema = z.object({
|
||||
toolCallId: z.string().min(1, 'Tool call ID is required'),
|
||||
status: z.enum(
|
||||
Object.values(ASYNC_TOOL_CONFIRMATION_STATUS) as [
|
||||
AsyncConfirmationStatus,
|
||||
...AsyncConfirmationStatus[],
|
||||
],
|
||||
{ error: 'Invalid notification status' }
|
||||
),
|
||||
message: z.string().optional(),
|
||||
data: z.unknown().optional(),
|
||||
})
|
||||
export type CopilotConfirmBody = z.input<typeof copilotConfirmBodySchema>
|
||||
|
||||
export const createWorkflowCopilotChatBodySchema = z.object({
|
||||
workspaceId: z.string().min(1),
|
||||
workflowId: z.string().min(1),
|
||||
})
|
||||
export type CreateWorkflowCopilotChatBody = z.input<typeof createWorkflowCopilotChatBodySchema>
|
||||
|
||||
export const copilotTrainingExampleBodySchema = z.object({
|
||||
json: z.string().min(1, 'JSON string is required'),
|
||||
title: z.string().min(1, 'Title is required'),
|
||||
tags: z.array(z.string()).optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
export type CopilotTrainingExampleBody = z.input<typeof copilotTrainingExampleBodySchema>
|
||||
|
||||
const copilotTrainingOperationSchema = z.object({
|
||||
operation_type: z.string(),
|
||||
block_id: z.string(),
|
||||
params: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
|
||||
export const copilotTrainingDataBodySchema = z.object({
|
||||
title: z.string().min(1, 'Title is required'),
|
||||
prompt: z.string().min(1, 'Prompt is required'),
|
||||
input: z.record(z.string(), z.unknown()),
|
||||
output: z.record(z.string(), z.unknown()),
|
||||
operations: z.array(copilotTrainingOperationSchema),
|
||||
})
|
||||
export type CopilotTrainingDataBody = z.input<typeof copilotTrainingDataBodySchema>
|
||||
|
||||
export const renameCopilotChatBodySchema = z.object({
|
||||
chatId: z.string().min(1),
|
||||
title: z.string().min(1).max(200),
|
||||
})
|
||||
export type RenameCopilotChatBody = z.input<typeof renameCopilotChatBodySchema>
|
||||
|
||||
const copilotResourceTypeSchema = z.enum([
|
||||
'table',
|
||||
'file',
|
||||
'workflow',
|
||||
'knowledgebase',
|
||||
'folder',
|
||||
'scheduledtask',
|
||||
'log',
|
||||
])
|
||||
|
||||
export const addCopilotChatResourceBodySchema = z.object({
|
||||
chatId: z.string(),
|
||||
resource: z.object({
|
||||
type: copilotResourceTypeSchema,
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
})
|
||||
export type AddCopilotChatResourceBody = z.input<typeof addCopilotChatResourceBodySchema>
|
||||
|
||||
export const removeCopilotChatResourceBodySchema = z.object({
|
||||
chatId: z.string(),
|
||||
resourceType: copilotResourceTypeSchema,
|
||||
resourceId: z.string(),
|
||||
})
|
||||
export type RemoveCopilotChatResourceBody = z.input<typeof removeCopilotChatResourceBodySchema>
|
||||
|
||||
export const reorderCopilotChatResourcesBodySchema = z.object({
|
||||
chatId: z.string(),
|
||||
resources: z.array(
|
||||
z.object({
|
||||
type: copilotResourceTypeSchema,
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
export type ReorderCopilotChatResourcesBody = z.input<typeof reorderCopilotChatResourcesBodySchema>
|
||||
|
||||
export const revertCopilotCheckpointBodySchema = z.object({
|
||||
checkpointId: z.string().min(1),
|
||||
})
|
||||
export type RevertCopilotCheckpointBody = z.input<typeof revertCopilotCheckpointBodySchema>
|
||||
|
||||
export const copilotChatAbortBodySchema = z.object({
|
||||
streamId: z.string().optional(),
|
||||
chatId: z.string().optional(),
|
||||
})
|
||||
export type CopilotChatAbortBody = z.input<typeof copilotChatAbortBodySchema>
|
||||
|
||||
export const copilotChatGetQuerySchema = z
|
||||
.object({
|
||||
workflowId: z.string().optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
chatId: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const copilotModelsQuerySchema = z.object({})
|
||||
|
||||
export const createCopilotCheckpointBodySchema = z.object({
|
||||
workflowId: z.string(),
|
||||
chatId: z.string(),
|
||||
messageId: z.string().optional(),
|
||||
workflowState: z.string(),
|
||||
})
|
||||
export type CreateCopilotCheckpointBody = z.input<typeof createCopilotCheckpointBodySchema>
|
||||
|
||||
export const listCopilotCheckpointsQuerySchema = z.object({
|
||||
chatId: z.string({ error: 'chatId is required' }).min(1, 'chatId is required'),
|
||||
})
|
||||
export type ListCopilotCheckpointsQuery = z.input<typeof listCopilotCheckpointsQuerySchema>
|
||||
|
||||
export const copilotChatStreamQuerySchema = z.object({
|
||||
streamId: z.string().optional().default(''),
|
||||
after: z.string().optional().default(''),
|
||||
batch: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) => value === 'true'),
|
||||
})
|
||||
|
||||
const storedToolCallSchema = z
|
||||
.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
state: z.string().optional(),
|
||||
params: z.record(z.string(), z.unknown()).optional(),
|
||||
result: z
|
||||
.object({
|
||||
success: z.boolean(),
|
||||
output: z.unknown().optional(),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
display: z
|
||||
.object({
|
||||
text: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
phaseLabel: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
calledBy: z.string().optional(),
|
||||
durationMs: z.number().optional(),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
.nullable()
|
||||
|
||||
const copilotContentBlockSchema = z.object({
|
||||
type: z.string(),
|
||||
lane: z.enum(['main', 'subagent']).optional(),
|
||||
content: z.string().optional(),
|
||||
channel: z.enum(['assistant', 'thinking']).optional(),
|
||||
phase: z.enum(['call', 'args_delta', 'result']).optional(),
|
||||
kind: z.enum(['subagent', 'structured_result', 'subagent_result']).optional(),
|
||||
lifecycle: z.enum(['start', 'end']).optional(),
|
||||
status: z.enum(['complete', 'error', 'cancelled']).optional(),
|
||||
parentToolCallId: z.string().optional(),
|
||||
toolCall: storedToolCallSchema.optional(),
|
||||
timestamp: z.number().optional(),
|
||||
endedAt: z.number().optional(),
|
||||
})
|
||||
|
||||
export const copilotChatStopBodySchema = z.object({
|
||||
chatId: z.string(),
|
||||
streamId: z.string(),
|
||||
content: z.string(),
|
||||
contentBlocks: z.array(copilotContentBlockSchema).optional(),
|
||||
requestId: z.string().optional(),
|
||||
})
|
||||
export type CopilotChatStopBody = z.input<typeof copilotChatStopBodySchema>
|
||||
|
||||
export const deleteCopilotChatBodySchema = z.object({
|
||||
chatId: z.string(),
|
||||
})
|
||||
export type DeleteCopilotChatBody = z.input<typeof deleteCopilotChatBodySchema>
|
||||
|
||||
const copilotPersistedMessageSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
role: z.enum(['user', 'assistant', 'system']),
|
||||
content: z.string(),
|
||||
timestamp: z.string(),
|
||||
toolCalls: z.array(z.any()).optional(),
|
||||
contentBlocks: z.array(z.any()).optional(),
|
||||
fileAttachments: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
key: z.string(),
|
||||
filename: z.string(),
|
||||
media_type: z.string(),
|
||||
size: z.number(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
contexts: z.array(z.any()).optional(),
|
||||
citations: z.array(z.any()).optional(),
|
||||
errorType: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const updateCopilotMessagesBodySchema = z.object({
|
||||
chatId: z.string(),
|
||||
messages: z.array(copilotPersistedMessageSchema),
|
||||
planArtifact: z.string().nullable().optional(),
|
||||
config: z
|
||||
.object({
|
||||
mode: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
})
|
||||
export type UpdateCopilotMessagesBody = z.input<typeof updateCopilotMessagesBodySchema>
|
||||
|
||||
export const validateCopilotApiKeyBodySchema = z.object({
|
||||
userId: z.string().min(1, 'userId is required'),
|
||||
/**
|
||||
* Originating workspace. Used to enforce per-member org-workspace credit limits
|
||||
* at mothership/copilot request time. Required: the Go mothership always resolves
|
||||
* a workspace for a chat request, so a missing value must fail closed (block the
|
||||
* request) rather than silently skip the per-member gate.
|
||||
*/
|
||||
workspaceId: z.string().min(1),
|
||||
})
|
||||
export type ValidateCopilotApiKeyBody = z.input<typeof validateCopilotApiKeyBodySchema>
|
||||
|
||||
export const listCopilotApiKeysContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/copilot/api-keys',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
keys: z.array(copilotApiKeySchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const copilotChatListItemSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string().nullable(),
|
||||
workflowId: z.string().nullable().optional(),
|
||||
workspaceId: z.string().nullable().optional(),
|
||||
activeStreamId: z.string().nullable(),
|
||||
updatedAt: z.string().nullable(),
|
||||
})
|
||||
export type CopilotChatListItem = z.output<typeof copilotChatListItemSchema>
|
||||
|
||||
export const listCopilotChatsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/copilot/chats',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
chats: z.array(copilotChatListItemSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const generateCopilotApiKeyContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/copilot/api-keys/generate',
|
||||
body: generateCopilotApiKeyBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
key: z.object({
|
||||
id: z.string(),
|
||||
apiKey: z.string(),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export type GenerateCopilotApiKeyResult = ContractJsonResponse<typeof generateCopilotApiKeyContract>
|
||||
|
||||
export const deleteCopilotApiKeyContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/copilot/api-keys',
|
||||
query: deleteCopilotApiKeyQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const submitCopilotFeedbackContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/copilot/feedback',
|
||||
body: submitCopilotFeedbackBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
feedbackId: z.string(),
|
||||
message: z.string(),
|
||||
metadata: z.object({
|
||||
requestId: z.string(),
|
||||
duration: z.number(),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export type SubmitCopilotFeedbackResult = ContractJsonResponse<typeof submitCopilotFeedbackContract>
|
||||
|
||||
const successFlagSchema = z.object({ success: z.literal(true) })
|
||||
|
||||
const copilotCheckpointSchema = z.object({
|
||||
id: z.string(),
|
||||
userId: z.string(),
|
||||
workflowId: z.string(),
|
||||
chatId: z.string(),
|
||||
messageId: z.string().nullable().optional(),
|
||||
createdAt: z.string().nullable(),
|
||||
updatedAt: z.string().nullable(),
|
||||
})
|
||||
|
||||
const copilotChatResourceSchema = z.object({
|
||||
type: copilotResourceTypeSchema,
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
})
|
||||
|
||||
const copilotAvailableModelSchema = z.object({
|
||||
id: z.string(),
|
||||
friendlyName: z.string(),
|
||||
provider: z.string(),
|
||||
})
|
||||
|
||||
const copilotChatGetChatSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
title: z.string().nullable(),
|
||||
model: z.string().nullable(),
|
||||
messages: z.array(z.unknown()),
|
||||
messageCount: z.number(),
|
||||
planArtifact: z.unknown().nullable(),
|
||||
config: z.unknown().nullable(),
|
||||
activeStreamId: z.string().nullable().optional(),
|
||||
resources: z.array(z.unknown()).optional(),
|
||||
createdAt: z.string().nullable(),
|
||||
updatedAt: z.string().nullable(),
|
||||
streamSnapshot: z
|
||||
.object({
|
||||
events: z.array(z.unknown()),
|
||||
previewSessions: z.array(z.unknown()),
|
||||
status: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const copilotChatGetListItemSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
title: z.string().nullable(),
|
||||
model: z.string().nullable(),
|
||||
createdAt: z.string().nullable(),
|
||||
updatedAt: z.string().nullable(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const copilotConnectedCredentialSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
provider: z.string(),
|
||||
serviceName: z.string(),
|
||||
lastUsed: z.string(),
|
||||
isDefault: z.boolean(),
|
||||
})
|
||||
|
||||
const copilotNotConnectedServiceSchema = z.object({
|
||||
providerId: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
baseProvider: z.string(),
|
||||
})
|
||||
|
||||
const copilotCredentialsResultSchema = z.object({
|
||||
oauth: z.object({
|
||||
connected: z.object({
|
||||
credentials: z.array(copilotConnectedCredentialSchema),
|
||||
total: z.number(),
|
||||
}),
|
||||
notConnected: z.object({
|
||||
services: z.array(copilotNotConnectedServiceSchema),
|
||||
total: z.number(),
|
||||
}),
|
||||
}),
|
||||
environment: z.object({
|
||||
variableNames: z.array(z.string()),
|
||||
count: z.number(),
|
||||
personalVariables: z.array(z.string()),
|
||||
workspaceVariables: z.array(z.string()),
|
||||
conflicts: z.array(z.string()),
|
||||
}),
|
||||
})
|
||||
|
||||
export const copilotCredentialsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/copilot/credentials',
|
||||
query: copilotCredentialsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
result: copilotCredentialsResultSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const validateCopilotApiKeyContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/copilot/api-keys/validate',
|
||||
body: validateCopilotApiKeyBodySchema,
|
||||
response: { mode: 'empty' },
|
||||
})
|
||||
|
||||
export const validateCopilotByokBodySchema = z.object({
|
||||
workspaceId: z.string().min(1, 'workspaceId is required'),
|
||||
userId: z.string().min(1, 'userId is required'),
|
||||
})
|
||||
export type ValidateCopilotByokBody = z.input<typeof validateCopilotByokBodySchema>
|
||||
|
||||
/**
|
||||
* Server-to-server entitlement gate called by the mothership (Go) before it
|
||||
* uses a workspace's own provider key. Empty 200/401/403 responses signal the
|
||||
* outcome; the Go caller fails closed to hosted keys on anything but a 200.
|
||||
*/
|
||||
export const validateCopilotByokContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/copilot/byok/validate',
|
||||
body: validateCopilotByokBodySchema,
|
||||
response: { mode: 'empty' },
|
||||
})
|
||||
|
||||
export const listCopilotByokKeysQuerySchema = z.object({
|
||||
workspaceId: z.string().min(1, 'workspaceId is required'),
|
||||
})
|
||||
export type ListCopilotByokKeysQuery = z.input<typeof listCopilotByokKeysQuerySchema>
|
||||
|
||||
export const upsertCopilotByokKeyBodySchema = z.object({
|
||||
workspaceId: z.string().min(1, 'workspaceId is required'),
|
||||
provider: z.string().min(1, 'provider is required'),
|
||||
apiKey: z.string().min(1, 'apiKey is required'),
|
||||
})
|
||||
export type UpsertCopilotByokKeyBody = z.input<typeof upsertCopilotByokKeyBodySchema>
|
||||
|
||||
export const deleteCopilotByokKeyQuerySchema = z.object({
|
||||
workspaceId: z.string().min(1, 'workspaceId is required'),
|
||||
provider: z.string().min(1, 'provider is required'),
|
||||
})
|
||||
export type DeleteCopilotByokKeyQuery = z.input<typeof deleteCopilotByokKeyQuerySchema>
|
||||
|
||||
/**
|
||||
* Superuser-gated proxies to the copilot's `/api/admin/byok` endpoints. The
|
||||
* responses are owned by the copilot service and forwarded verbatim.
|
||||
*/
|
||||
export const listCopilotByokKeysContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/copilot/byok',
|
||||
query: listCopilotByokKeysQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
// untyped-response: forwards the copilot /api/admin/byok response unchanged; shape is owned by the copilot service
|
||||
schema: z.unknown(),
|
||||
},
|
||||
})
|
||||
|
||||
export const upsertCopilotByokKeyContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/copilot/byok',
|
||||
body: upsertCopilotByokKeyBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
// untyped-response: forwards the copilot /api/admin/byok response unchanged; shape is owned by the copilot service
|
||||
schema: z.unknown(),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteCopilotByokKeyContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/copilot/byok',
|
||||
query: deleteCopilotByokKeyQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
// untyped-response: forwards the copilot /api/admin/byok response unchanged; shape is owned by the copilot service
|
||||
schema: z.unknown(),
|
||||
},
|
||||
})
|
||||
|
||||
export const createWorkflowCopilotChatContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/copilot/chats',
|
||||
body: createWorkflowCopilotChatBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
id: z.string(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const createCopilotCheckpointContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/copilot/checkpoints',
|
||||
body: createCopilotCheckpointBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
checkpoint: copilotCheckpointSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const listCopilotCheckpointsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/copilot/checkpoints',
|
||||
query: listCopilotCheckpointsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
checkpoints: z.array(copilotCheckpointSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const copilotConfirmContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/copilot/confirm',
|
||||
body: copilotConfirmBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
message: z.string(),
|
||||
toolCallId: z.string(),
|
||||
status: z.string(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const copilotModelsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/copilot/models',
|
||||
query: copilotModelsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
models: z.array(copilotAvailableModelSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const addCopilotChatResourceContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/copilot/chat/resources',
|
||||
body: addCopilotChatResourceBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
resources: z.array(copilotChatResourceSchema).optional(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const reorderCopilotChatResourcesContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/copilot/chat/resources',
|
||||
body: reorderCopilotChatResourcesBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
resources: z.array(copilotChatResourceSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const removeCopilotChatResourceContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/copilot/chat/resources',
|
||||
body: removeCopilotChatResourceBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
resources: z.array(copilotChatResourceSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Forwards the agent indexer's free-form JSON response.
|
||||
* Shape varies by upstream version.
|
||||
*/
|
||||
export const copilotTrainingDataContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/copilot/training',
|
||||
body: copilotTrainingDataBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
// untyped-response: forwards external agent indexer /operations/add response unchanged; shape varies by upstream version
|
||||
schema: z.unknown(),
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Forwards the agent indexer's free-form JSON response.
|
||||
* Shape varies by upstream version.
|
||||
*/
|
||||
export const copilotTrainingExampleContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/copilot/training/examples',
|
||||
body: copilotTrainingExampleBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
// untyped-response: forwards external agent indexer /examples/add response unchanged; shape varies by upstream version
|
||||
schema: z.unknown(),
|
||||
},
|
||||
})
|
||||
|
||||
export const renameCopilotChatContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/copilot/chat/rename',
|
||||
body: renameCopilotChatBodySchema,
|
||||
response: { mode: 'json', schema: successFlagSchema },
|
||||
})
|
||||
|
||||
export const revertCopilotCheckpointContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/copilot/checkpoints/revert',
|
||||
body: revertCopilotCheckpointBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
workflowId: z.string(),
|
||||
checkpointId: z.string(),
|
||||
revertedAt: z.string(),
|
||||
checkpoint: z.object({
|
||||
id: z.string(),
|
||||
workflowState: cleanedWorkflowStateSchema,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const copilotChatAbortContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/copilot/chat/abort',
|
||||
body: copilotChatAbortBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
aborted: z.boolean(),
|
||||
settled: z.boolean().optional(),
|
||||
// True when the stream did not settle within the grace window and the
|
||||
// chat stream lock was force-broken so the chat is immediately usable.
|
||||
forceReleased: z.boolean().optional(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const copilotChatStreamContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/copilot/chat/stream',
|
||||
query: copilotChatStreamQuerySchema,
|
||||
response: { mode: 'stream' },
|
||||
})
|
||||
|
||||
export const copilotChatStopContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/copilot/chat/stop',
|
||||
body: copilotChatStopBodySchema,
|
||||
response: { mode: 'json', schema: successFlagSchema },
|
||||
})
|
||||
|
||||
export const copilotChatGetContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/copilot/chat',
|
||||
query: copilotChatGetQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.union([
|
||||
z.object({
|
||||
success: z.literal(true),
|
||||
chat: copilotChatGetChatSchema,
|
||||
}),
|
||||
z.object({
|
||||
success: z.literal(true),
|
||||
chats: z.array(copilotChatGetListItemSchema),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteCopilotChatContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/copilot/chat/delete',
|
||||
body: deleteCopilotChatBodySchema,
|
||||
response: { mode: 'json', schema: successFlagSchema },
|
||||
})
|
||||
|
||||
export const updateCopilotMessagesContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/copilot/chat/update-messages',
|
||||
body: updateCopilotMessagesBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
messageCount: z.number(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,439 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import {
|
||||
ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID,
|
||||
type OAuthProvider,
|
||||
SLACK_CUSTOM_BOT_PROVIDER_ID,
|
||||
} from '@/lib/oauth/types'
|
||||
|
||||
const ENV_VAR_NAME_REGEX = /^[A-Za-z0-9_]+$/
|
||||
|
||||
export function normalizeCredentialEnvKey(raw: string): string {
|
||||
const trimmed = raw.trim()
|
||||
const wrappedMatch = /^\{\{\s*([A-Za-z0-9_]+)\s*\}\}$/.exec(trimmed)
|
||||
return wrappedMatch ? wrappedMatch[1] : trimmed
|
||||
}
|
||||
|
||||
export const workspaceCredentialTypeSchema = z.enum([
|
||||
'oauth',
|
||||
'env_workspace',
|
||||
'env_personal',
|
||||
'service_account',
|
||||
])
|
||||
export const workspaceCredentialRoleSchema = z.enum(['admin', 'member'])
|
||||
export const workspaceCredentialMemberStatusSchema = z.enum(['active', 'pending', 'revoked'])
|
||||
|
||||
export const workspaceCredentialSchema = z.object({
|
||||
id: z.string(),
|
||||
workspaceId: z.string(),
|
||||
type: workspaceCredentialTypeSchema,
|
||||
displayName: z.string(),
|
||||
description: z.string().nullable(),
|
||||
providerId: z.string().nullable(),
|
||||
accountId: z.string().nullable(),
|
||||
envKey: z.string().nullable(),
|
||||
envOwnerUserId: z.string().nullable(),
|
||||
createdBy: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
role: workspaceCredentialRoleSchema.optional(),
|
||||
status: workspaceCredentialMemberStatusSchema.optional(),
|
||||
})
|
||||
|
||||
export type WorkspaceCredentialType = z.output<typeof workspaceCredentialTypeSchema>
|
||||
export type WorkspaceCredentialRole = z.output<typeof workspaceCredentialRoleSchema>
|
||||
export type WorkspaceCredentialMemberStatus = z.output<typeof workspaceCredentialMemberStatusSchema>
|
||||
export type WorkspaceCredential = z.output<typeof workspaceCredentialSchema>
|
||||
|
||||
export const credentialsListQuerySchema = z.object({
|
||||
workspaceId: z.string().uuid('Workspace ID must be a valid UUID'),
|
||||
type: workspaceCredentialTypeSchema.optional(),
|
||||
providerId: z.string().optional(),
|
||||
})
|
||||
|
||||
export const credentialIdParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const credentialsListGetQuerySchema = z.object({
|
||||
workspaceId: z.string().uuid('Workspace ID must be a valid UUID'),
|
||||
type: workspaceCredentialTypeSchema.optional(),
|
||||
providerId: z.string().optional(),
|
||||
credentialId: z.string().optional(),
|
||||
})
|
||||
|
||||
export const serviceAccountJsonSchema = z
|
||||
.string()
|
||||
.min(1, 'Service account JSON key is required')
|
||||
.transform((val, ctx) => {
|
||||
try {
|
||||
const parsed = JSON.parse(val)
|
||||
if (parsed.type !== 'service_account') {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'JSON key must have type "service_account"',
|
||||
})
|
||||
return z.NEVER
|
||||
}
|
||||
if (!parsed.client_email || typeof parsed.client_email !== 'string') {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'JSON key must contain a valid client_email',
|
||||
})
|
||||
return z.NEVER
|
||||
}
|
||||
if (!parsed.private_key || typeof parsed.private_key !== 'string') {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'JSON key must contain a valid private_key',
|
||||
})
|
||||
return z.NEVER
|
||||
}
|
||||
if (!parsed.project_id || typeof parsed.project_id !== 'string') {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'JSON key must contain a valid project_id',
|
||||
})
|
||||
return z.NEVER
|
||||
}
|
||||
return parsed as {
|
||||
type: 'service_account'
|
||||
client_email: string
|
||||
private_key: string
|
||||
project_id: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
} catch {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Invalid JSON format',
|
||||
})
|
||||
return z.NEVER
|
||||
}
|
||||
})
|
||||
|
||||
export const createCredentialBodySchema = z
|
||||
.object({
|
||||
workspaceId: z.string().uuid('Workspace ID must be a valid UUID'),
|
||||
type: workspaceCredentialTypeSchema,
|
||||
displayName: z.string().trim().min(1).max(255).optional(),
|
||||
description: z.string().trim().max(500).optional(),
|
||||
providerId: z.string().trim().min(1).optional(),
|
||||
accountId: z.string().trim().min(1).optional(),
|
||||
envKey: z.string().trim().min(1).optional(),
|
||||
envOwnerUserId: z.string().trim().min(1).optional(),
|
||||
serviceAccountJson: z.string().optional(),
|
||||
apiToken: z.string().trim().min(1).optional(),
|
||||
domain: z.string().trim().min(1).optional(),
|
||||
/**
|
||||
* Client-supplied credential id, honored only for `slack-custom-bot` creates:
|
||||
* the setup modal shows the ingest URL `/api/webhooks/slack/custom/{id}`
|
||||
* before secrets exist, so the id must be known up front.
|
||||
*/
|
||||
id: z.string().uuid('id must be a valid UUID').optional(),
|
||||
signingSecret: z.string().trim().min(1).optional(),
|
||||
botToken: z.string().trim().min(1).optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.type === 'oauth') {
|
||||
if (!data.accountId) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'accountId is required for oauth credentials',
|
||||
path: ['accountId'],
|
||||
})
|
||||
}
|
||||
if (!data.providerId) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'providerId is required for oauth credentials',
|
||||
path: ['providerId'],
|
||||
})
|
||||
}
|
||||
if (!data.displayName) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'displayName is required for oauth credentials',
|
||||
path: ['displayName'],
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type === 'service_account') {
|
||||
if (data.providerId === ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID) {
|
||||
if (!data.apiToken) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'apiToken is required for Atlassian service account credentials',
|
||||
path: ['apiToken'],
|
||||
})
|
||||
}
|
||||
if (!data.domain) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'domain is required for Atlassian service account credentials',
|
||||
path: ['domain'],
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if (data.providerId === SLACK_CUSTOM_BOT_PROVIDER_ID) {
|
||||
if (!data.signingSecret) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'signingSecret is required for a custom Slack bot credential',
|
||||
path: ['signingSecret'],
|
||||
})
|
||||
}
|
||||
if (!data.botToken) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'botToken is required for a custom Slack bot credential',
|
||||
path: ['botToken'],
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!data.serviceAccountJson) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'serviceAccountJson is required for service account credentials',
|
||||
path: ['serviceAccountJson'],
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedEnvKey = data.envKey ? normalizeCredentialEnvKey(data.envKey) : ''
|
||||
if (!normalizedEnvKey) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'envKey is required for env credentials',
|
||||
path: ['envKey'],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!ENV_VAR_NAME_REGEX.test(normalizedEnvKey)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'envKey must contain only letters, numbers, and underscores',
|
||||
path: ['envKey'],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export const updateCredentialByIdBodySchema = z
|
||||
.object({
|
||||
displayName: z.string().trim().min(1).max(255).optional(),
|
||||
description: z.string().trim().max(500).nullish(),
|
||||
serviceAccountJson: z.string().min(1).optional(),
|
||||
/** Slack custom-bot secret rotation (reconnect). */
|
||||
signingSecret: z.string().trim().min(1).optional(),
|
||||
botToken: z.string().trim().min(1).optional(),
|
||||
/** Atlassian service-account secret rotation (reconnect). */
|
||||
apiToken: z.string().trim().min(1).optional(),
|
||||
domain: z.string().trim().min(1).optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine(
|
||||
(data) =>
|
||||
data.displayName !== undefined ||
|
||||
data.description !== undefined ||
|
||||
data.serviceAccountJson !== undefined ||
|
||||
data.signingSecret !== undefined ||
|
||||
data.botToken !== undefined ||
|
||||
data.apiToken !== undefined ||
|
||||
data.domain !== undefined,
|
||||
{
|
||||
message: 'At least one field must be provided',
|
||||
path: ['displayName'],
|
||||
}
|
||||
)
|
||||
|
||||
export const leaveCredentialQuerySchema = z.object({
|
||||
credentialId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const workspaceCredentialMemberSchema = z.object({
|
||||
id: z.string(),
|
||||
userId: z.string(),
|
||||
role: workspaceCredentialRoleSchema,
|
||||
status: workspaceCredentialMemberStatusSchema,
|
||||
joinedAt: z.string().nullable(),
|
||||
invitedBy: z.string().nullable().optional(),
|
||||
createdAt: z.string().optional(),
|
||||
updatedAt: z.string().optional(),
|
||||
userName: z.string().nullable(),
|
||||
userEmail: z.string().nullable(),
|
||||
userImage: z.string().nullable().optional(),
|
||||
/** `workspace-admin` roles are derived from workspace admin and cannot be changed. */
|
||||
roleSource: z.enum(['explicit', 'workspace-admin']).optional(),
|
||||
})
|
||||
|
||||
export type WorkspaceCredentialMember = z.output<typeof workspaceCredentialMemberSchema>
|
||||
|
||||
export const createCredentialDraftBodySchema = z.object({
|
||||
workspaceId: z.string().min(1),
|
||||
providerId: z.string().min(1),
|
||||
displayName: z.string().min(1),
|
||||
description: z.string().trim().max(500).optional(),
|
||||
credentialId: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
export const upsertWorkspaceCredentialMemberBodySchema = z.object({
|
||||
userId: z.string().min(1),
|
||||
role: workspaceCredentialRoleSchema.default('member'),
|
||||
})
|
||||
|
||||
export const removeWorkspaceCredentialMemberQuerySchema = z.object({
|
||||
userId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const oauthCredentialSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
provider: z.custom<OAuthProvider>((value) => typeof value === 'string'),
|
||||
type: z.enum(['oauth', 'service_account']).optional(),
|
||||
serviceId: z.string().optional(),
|
||||
lastUsed: z.string().optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
scopes: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export const oauthCredentialsQuerySchema = z
|
||||
.object({
|
||||
provider: z.string().nullish(),
|
||||
workflowId: z.string().uuid('Workflow ID must be a valid UUID').nullish(),
|
||||
workspaceId: z.string().uuid('Workspace ID must be a valid UUID').nullish(),
|
||||
credentialId: z.string().min(1, 'Credential ID must not be empty').max(255).nullish(),
|
||||
})
|
||||
.refine((data) => data.provider || data.credentialId, {
|
||||
message: 'Provider or credentialId is required',
|
||||
path: ['provider'],
|
||||
})
|
||||
|
||||
export const listWorkspaceCredentialsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/credentials',
|
||||
query: credentialsListQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
credentials: z.array(workspaceCredentialSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const getWorkspaceCredentialContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/credentials/[id]',
|
||||
params: credentialIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
credential: workspaceCredentialSchema.nullable(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const listOAuthCredentialsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/auth/oauth/credentials',
|
||||
query: oauthCredentialsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
credentials: z.array(oauthCredentialSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const listWorkspaceCredentialMembersContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/credentials/[id]/members',
|
||||
params: credentialIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
members: z.array(workspaceCredentialMemberSchema).optional(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const createCredentialDraftContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/credentials/draft',
|
||||
body: createCredentialDraftBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const createWorkspaceCredentialContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/credentials',
|
||||
body: createCredentialBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
credential: workspaceCredentialSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateWorkspaceCredentialContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/credentials/[id]',
|
||||
params: credentialIdParamsSchema,
|
||||
body: updateCredentialByIdBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
credential: workspaceCredentialSchema.nullable(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteWorkspaceCredentialContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/credentials/[id]',
|
||||
params: credentialIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const upsertWorkspaceCredentialMemberContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/credentials/[id]/members',
|
||||
params: credentialIdParamsSchema,
|
||||
body: upsertWorkspaceCredentialMemberBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
member: workspaceCredentialMemberSchema.optional(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const removeWorkspaceCredentialMemberContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/credentials/[id]/members',
|
||||
params: credentialIdParamsSchema,
|
||||
query: removeWorkspaceCredentialMemberQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
}),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,178 @@
|
||||
import { z } from 'zod'
|
||||
import { workflowIdSchema, workspaceIdSchema } from '@/lib/api/contracts/primitives'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const inputFieldSchema = z.object({
|
||||
/** Stable per-field id — preserved so client block configs key sub-blocks on it
|
||||
* (rename-safe wiring) instead of the display name. Absent on legacy fields. */
|
||||
id: z.string().optional(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
description: z.string().optional(),
|
||||
/** Consumer-facing placeholder hint (curated inputs only). */
|
||||
placeholder: z.string().optional(),
|
||||
/** Consumers must fill this input (curated inputs only). */
|
||||
required: z.boolean().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* The authored per-input data: a placeholder and a required flag, keyed by the
|
||||
* source Start field's stable `id`. The field's name/type/description are NOT
|
||||
* stored — they're always derived from the live deployed Start (so they can't go
|
||||
* stale); an override whose field was removed from the Start is silently ignored.
|
||||
*/
|
||||
const inputPlaceholderSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
placeholder: z.string().max(200).optional(),
|
||||
required: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type CustomBlockInputPlaceholder = z.input<typeof inputPlaceholderSchema>
|
||||
|
||||
/** A curated output: a child-workflow block output (blockId + dot-path) exposed under `name`. */
|
||||
const exposedOutputSchema = z.object({
|
||||
blockId: z.string().min(1),
|
||||
path: z.string().min(1),
|
||||
name: z.string().min(1).max(60),
|
||||
})
|
||||
|
||||
export const customBlockSchema = z.object({
|
||||
id: z.string(),
|
||||
organizationId: z.string(),
|
||||
workflowId: z.string(),
|
||||
/** Name of the bound source workflow (for display; the source can't be changed). */
|
||||
workflowName: z.string(),
|
||||
/** Source workflow's home workspace id — used client-side to gate manage affordances. */
|
||||
workspaceId: z.string().nullable(),
|
||||
/** Name of the source workflow's home workspace (display only). */
|
||||
workspaceName: z.string().nullable(),
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
/** Uploaded icon image URL, or null for the default icon. */
|
||||
iconUrl: z.string().nullable(),
|
||||
enabled: z.boolean(),
|
||||
inputFields: z.array(inputFieldSchema),
|
||||
/** Curated outputs exposed to consumers; empty = expose the child's whole result. */
|
||||
exposedOutputs: z.array(exposedOutputSchema),
|
||||
})
|
||||
|
||||
export type CustomBlock = z.output<typeof customBlockSchema>
|
||||
|
||||
export const listCustomBlocksQuerySchema = z.object({
|
||||
workspaceId: workspaceIdSchema,
|
||||
})
|
||||
|
||||
/**
|
||||
* Icon URLs are rendered as org-wide `<img>` sources, so only https URLs and
|
||||
* internal file-serve paths (what the icon upload UI stores) are accepted —
|
||||
* never data:/blob:/other schemes an admin could smuggle into shared metadata.
|
||||
* Shared with the copilot deploy_custom_block handler's pass-through branch.
|
||||
*/
|
||||
export function isAllowedCustomBlockIconUrl(value: string): boolean {
|
||||
return value.startsWith('https://') || value.startsWith('/api/files/serve/')
|
||||
}
|
||||
|
||||
const iconUrlSchema = z.string().min(1).max(2048).refine(isAllowedCustomBlockIconUrl, {
|
||||
message: 'iconUrl must be an https URL or an internal /api/files/serve/ path',
|
||||
})
|
||||
|
||||
export const publishCustomBlockBodySchema = z.object({
|
||||
workspaceId: workspaceIdSchema,
|
||||
workflowId: workflowIdSchema,
|
||||
name: z.string().min(1, 'Name is required').max(60, 'Name must be 60 characters or fewer'),
|
||||
description: z.string().max(280, 'Description must be 280 characters or fewer').default(''),
|
||||
/** Uploaded icon image URL (https or internal serve path); omit for the default icon. */
|
||||
iconUrl: iconUrlSchema.optional(),
|
||||
/** Per-input placeholder hints keyed by Start field id; the field set itself is always derived from the deployment. */
|
||||
inputs: z.array(inputPlaceholderSchema).max(50).optional(),
|
||||
/** Curated outputs; omit/empty to expose the child's whole result. */
|
||||
exposedOutputs: z.array(exposedOutputSchema).max(50).optional(),
|
||||
})
|
||||
|
||||
export type PublishCustomBlockBody = z.input<typeof publishCustomBlockBodySchema>
|
||||
|
||||
export const customBlockIdParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const updateCustomBlockBodySchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(60).optional(),
|
||||
description: z.string().max(280).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
/** A URL (https or internal serve path) sets/replaces the icon; `null` clears it (default icon). */
|
||||
iconUrl: iconUrlSchema.nullable().optional(),
|
||||
inputs: z.array(inputPlaceholderSchema).max(50).optional(),
|
||||
exposedOutputs: z.array(exposedOutputSchema).max(50).optional(),
|
||||
})
|
||||
.refine((v) => Object.keys(v).length > 0, { message: 'At least one field is required' })
|
||||
|
||||
export type UpdateCustomBlockBody = z.input<typeof updateCustomBlockBodySchema>
|
||||
|
||||
/**
|
||||
* How many workflows in the org place this block. Live editor state and the
|
||||
* active deployment snapshot can diverge, so a workflow counts when the block
|
||||
* appears in either; `deployedUsageCount` counts active deployments only.
|
||||
*/
|
||||
export const customBlockUsageCountsSchema = z.object({
|
||||
usageCount: z.number().int().min(0),
|
||||
deployedUsageCount: z.number().int().min(0),
|
||||
})
|
||||
|
||||
export type CustomBlockUsageCounts = z.output<typeof customBlockUsageCountsSchema>
|
||||
|
||||
export const listCustomBlocksContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/custom-blocks',
|
||||
query: listCustomBlocksQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
/** Whether this workspace can publish/use custom blocks (feature flag + enterprise plan). */
|
||||
enabled: z.boolean(),
|
||||
customBlocks: z.array(customBlockSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const publishCustomBlockContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/custom-blocks',
|
||||
body: publishCustomBlockBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({ customBlock: customBlockSchema }),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateCustomBlockContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/custom-blocks/[id]',
|
||||
params: customBlockIdParamsSchema,
|
||||
body: updateCustomBlockBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({ success: z.literal(true) }),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteCustomBlockContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/custom-blocks/[id]',
|
||||
params: customBlockIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({ success: z.literal(true) }),
|
||||
},
|
||||
})
|
||||
|
||||
export const getCustomBlockUsageCountsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/custom-blocks/[id]/usages',
|
||||
params: customBlockIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: customBlockUsageCountsSchema,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,567 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import { validateExternalUrl } from '@/lib/core/security/input-validation'
|
||||
import { CADENCE_TYPES, DESTINATION_TYPES, SOURCE_TYPES } from '@/lib/data-drains/types'
|
||||
|
||||
/** AWS S3 bucket: 3-63 chars, lowercase alnum + . / -, see s3.ts for full rules. */
|
||||
const S3_BUCKET_NAME_RE = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/
|
||||
const S3_IPV4_LIKE_RE = /^(\d{1,3}\.){3}\d{1,3}$/
|
||||
const AWS_REGION_RE = /^[a-z]{2,}(-[a-z]+)+-\d+$/
|
||||
/** GCS bucket component: lowercase alnum + _ / -, start/end alnum. Mirrors gcs.ts. */
|
||||
const GCS_BUCKET_COMPONENT_RE = /^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$/
|
||||
const GOOGLE_RESERVED_PREFIX_RE = /^(goog|google|g00gle)/i
|
||||
const GOOGLE_CONTAINS_RE = /(google|g00gle)/i
|
||||
function validateGcsBucketComponents(v: string): string | null {
|
||||
if (v.length < 3 || v.length > 222) return 'bucket must be 3-222 characters'
|
||||
const components = v.split('.')
|
||||
for (const c of components) {
|
||||
if (c.length < 1 || c.length > 63) {
|
||||
return 'each dot-separated component must be 1-63 characters'
|
||||
}
|
||||
if (!GCS_BUCKET_COMPONENT_RE.test(c)) {
|
||||
return 'each component must be lowercase, start/end alphanumeric, letters/digits/_/- only'
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
/** Azure storage account: 3-24 lowercase alnum. */
|
||||
const AZURE_ACCOUNT_NAME_RE = /^[a-z0-9]{3,24}$/
|
||||
/** Azure container: 3-63 chars, lowercase alnum + single hyphens. */
|
||||
const AZURE_CONTAINER_NAME_RE = /^[a-z0-9]([a-z0-9]|-(?!-))+[a-z0-9]$/
|
||||
/** Azure Blob Storage endpoint suffixes (Public, US Gov, China, Germany). */
|
||||
const AZURE_ENDPOINT_SUFFIXES = [
|
||||
'blob.core.windows.net',
|
||||
'blob.core.usgovcloudapi.net',
|
||||
'blob.core.chinacloudapi.cn',
|
||||
'blob.core.cloudapi.de',
|
||||
] as const
|
||||
/** BigQuery project / dataset / table identifiers. */
|
||||
const BQ_PROJECT_ID_RE = /^([a-z][a-z0-9.-]{0,61}[a-z0-9]:)?[a-z][a-z0-9-]{4,28}[a-z0-9]$/
|
||||
const BQ_DATASET_RE = /^[A-Za-z0-9_]{1,1024}$/
|
||||
const BQ_TABLE_RE = /^[\p{L}\p{M}\p{N}\p{Pc}\p{Pd} ]{1,1024}$/u
|
||||
/** Snowflake account + identifier shapes — mirrored from snowflake.ts. */
|
||||
const SNOWFLAKE_ACCOUNT_ORG_RE = /^[A-Za-z0-9][A-Za-z0-9_]*(?:-[A-Za-z0-9_]+)+$/
|
||||
/** First segment allows hyphens so org-account identifiers carrying a region/cloud suffix match. Mirrors snowflake.ts. */
|
||||
const SNOWFLAKE_ACCOUNT_LOCATOR_RE =
|
||||
/^[A-Za-z0-9][A-Za-z0-9_-]*(?:\.[A-Za-z0-9][A-Za-z0-9_-]*){0,2}$/
|
||||
const SNOWFLAKE_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_$]{0,254}$/
|
||||
/** Reserved Sim-namespaced header names that cannot be reused as the webhook signature header. */
|
||||
const RESERVED_WEBHOOK_SIGNATURE_HEADER_NAMES = new Set([
|
||||
'authorization',
|
||||
'content-type',
|
||||
'user-agent',
|
||||
'idempotency-key',
|
||||
'x-sim-timestamp',
|
||||
'x-sim-signature-version',
|
||||
'x-sim-drain-id',
|
||||
'x-sim-run-id',
|
||||
'x-sim-source',
|
||||
'x-sim-sequence',
|
||||
'x-sim-row-count',
|
||||
'x-sim-probe',
|
||||
'x-sim-signature',
|
||||
])
|
||||
|
||||
export const dataDrainSourceSchema = z.enum(SOURCE_TYPES)
|
||||
export const dataDrainDestinationTypeSchema = z.enum(DESTINATION_TYPES)
|
||||
export const dataDrainCadenceSchema = z.enum(CADENCE_TYPES)
|
||||
export const dataDrainRunStatusSchema = z.enum(['running', 'success', 'failed'])
|
||||
export const dataDrainRunTriggerSchema = z.enum(['cron', 'manual'])
|
||||
|
||||
export const dataDrainOrgParamsSchema = z.object({
|
||||
id: z.string().min(1, 'organization id is required'),
|
||||
})
|
||||
|
||||
export const dataDrainParamsSchema = z.object({
|
||||
id: z.string().min(1, 'organization id is required'),
|
||||
drainId: z.string().min(1, 'drain id is required'),
|
||||
})
|
||||
|
||||
const drainNameSchema = z.string().trim().min(1, 'name is required').max(120)
|
||||
|
||||
const s3ConfigBodySchema = z.object({
|
||||
bucket: z
|
||||
.string()
|
||||
.min(3, 'bucket must be 3-63 characters')
|
||||
.max(63, 'bucket must be 3-63 characters')
|
||||
.refine((v) => S3_BUCKET_NAME_RE.test(v), {
|
||||
message: 'bucket must be lowercase, 3-63 chars, start/end alphanumeric',
|
||||
})
|
||||
.refine((v) => !v.includes('..'), { message: 'bucket must not contain consecutive dots' })
|
||||
.refine((v) => !v.includes('-.') && !v.includes('.-'), {
|
||||
message: 'bucket must not contain a dash adjacent to a dot',
|
||||
})
|
||||
.refine((v) => !S3_IPV4_LIKE_RE.test(v), { message: 'bucket must not look like an IP address' })
|
||||
.refine((v) => !v.startsWith('xn--'), { message: 'bucket must not start with "xn--"' })
|
||||
.refine((v) => !v.startsWith('sthree-'), { message: 'bucket must not start with "sthree-"' })
|
||||
.refine((v) => !v.startsWith('amzn-s3-demo-'), {
|
||||
message: 'bucket must not start with "amzn-s3-demo-" (reserved by AWS)',
|
||||
})
|
||||
.refine(
|
||||
(v) =>
|
||||
!v.endsWith('-s3alias') &&
|
||||
!v.endsWith('--ol-s3') &&
|
||||
!v.endsWith('.mrap') &&
|
||||
!v.endsWith('--x-s3') &&
|
||||
!v.endsWith('--table-s3'),
|
||||
{
|
||||
message:
|
||||
'bucket must not end with reserved suffix (-s3alias, --ol-s3, .mrap, --x-s3, --table-s3)',
|
||||
}
|
||||
),
|
||||
region: z
|
||||
.string()
|
||||
.min(1, 'region is required')
|
||||
.max(32, 'region is too long')
|
||||
.refine((v) => AWS_REGION_RE.test(v), {
|
||||
message: 'region must look like an AWS region code, e.g. us-east-1',
|
||||
}),
|
||||
prefix: z
|
||||
.string()
|
||||
.max(512)
|
||||
.refine((v) => Buffer.byteLength(v, 'utf8') <= 512, {
|
||||
message: 'prefix must be at most 512 bytes (UTF-8)',
|
||||
})
|
||||
.optional(),
|
||||
endpoint: z
|
||||
.string()
|
||||
.url()
|
||||
.refine((v) => v.startsWith('https://'), { message: 'endpoint must use https://' })
|
||||
.refine((value) => validateExternalUrl(value, 'endpoint').isValid, {
|
||||
message: 'endpoint must be HTTPS and not point at a private, loopback, or metadata address',
|
||||
})
|
||||
.optional(),
|
||||
forcePathStyle: z.boolean().optional(),
|
||||
})
|
||||
|
||||
const s3CredentialsBodySchema = z.object({
|
||||
accessKeyId: z.string().min(1, 'accessKeyId is required'),
|
||||
secretAccessKey: z.string().min(1, 'secretAccessKey is required'),
|
||||
})
|
||||
|
||||
const gcsConfigBodySchema = z.object({
|
||||
bucket: z
|
||||
.string()
|
||||
.min(3, 'bucket must be 3-222 characters')
|
||||
.max(222, 'bucket must be 3-222 characters')
|
||||
.superRefine((v, ctx) => {
|
||||
const err = validateGcsBucketComponents(v)
|
||||
if (err) ctx.addIssue({ code: z.ZodIssueCode.custom, message: err })
|
||||
})
|
||||
.refine((v) => !S3_IPV4_LIKE_RE.test(v), { message: 'bucket must not look like an IP address' })
|
||||
.refine((v) => !v.includes('..'), { message: 'bucket must not contain consecutive dots' })
|
||||
.refine((v) => !v.includes('-.') && !v.includes('.-'), {
|
||||
message: 'bucket must not contain "-." or ".-"',
|
||||
})
|
||||
.refine((v) => !GOOGLE_RESERVED_PREFIX_RE.test(v) && !GOOGLE_CONTAINS_RE.test(v), {
|
||||
message: 'bucket name cannot begin with "goog" or contain "google" / close misspellings',
|
||||
}),
|
||||
prefix: z
|
||||
.string()
|
||||
.max(512)
|
||||
.refine((v) => Buffer.byteLength(v, 'utf8') <= 512, {
|
||||
message: 'prefix must be at most 512 bytes (UTF-8)',
|
||||
})
|
||||
.refine((v) => !v.startsWith('.well-known/acme-challenge/'), {
|
||||
message: 'prefix must not start with ".well-known/acme-challenge/" (reserved by GCS)',
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const gcsCredentialsBodySchema = z.object({
|
||||
serviceAccountJson: z.string().min(1, 'serviceAccountJson is required'),
|
||||
})
|
||||
|
||||
const azureBlobConfigBodySchema = z.object({
|
||||
accountName: z
|
||||
.string()
|
||||
.min(1, 'accountName is required')
|
||||
.refine((v) => AZURE_ACCOUNT_NAME_RE.test(v), {
|
||||
message: 'accountName must be 3-24 lowercase letters or digits',
|
||||
}),
|
||||
containerName: z
|
||||
.string()
|
||||
.min(3, 'containerName must be 3-63 characters')
|
||||
.max(63)
|
||||
.refine((v) => AZURE_CONTAINER_NAME_RE.test(v), {
|
||||
message: 'containerName must use lowercase letters, digits, or single hyphens',
|
||||
}),
|
||||
prefix: z.string().max(512).optional(),
|
||||
endpointSuffix: z
|
||||
.string()
|
||||
.refine((v) => (AZURE_ENDPOINT_SUFFIXES as readonly string[]).includes(v), {
|
||||
message: `endpointSuffix must be one of: ${AZURE_ENDPOINT_SUFFIXES.join(', ')}`,
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const azureBlobCredentialsBodySchema = z.object({
|
||||
accountKey: z
|
||||
.string()
|
||||
.length(88, 'accountKey must be 88 base64 characters (64-byte Azure storage key)')
|
||||
.regex(/^[A-Za-z0-9+/]+={0,2}$/, {
|
||||
message: 'accountKey must be a base64-encoded Azure storage account key',
|
||||
}),
|
||||
})
|
||||
|
||||
const DATADOG_TAG_PAIR_RE = /^[A-Za-z][A-Za-z0-9_./-]*:[^,\s][^,]*$/
|
||||
|
||||
const datadogConfigBodySchema = z.object({
|
||||
site: z.enum(['us1', 'us3', 'us5', 'eu1', 'ap1', 'ap2', 'gov']),
|
||||
service: z.string().min(1).max(100).optional(),
|
||||
tags: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(1024)
|
||||
.refine(
|
||||
(v) =>
|
||||
v
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t.length > 0)
|
||||
.every((t) => DATADOG_TAG_PAIR_RE.test(t)),
|
||||
{ message: 'tags must be comma-separated key:value pairs' }
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const datadogCredentialsBodySchema = z.object({
|
||||
apiKey: z.string().min(1, 'apiKey is required'),
|
||||
})
|
||||
|
||||
const bigqueryConfigBodySchema = z.object({
|
||||
projectId: z
|
||||
.string()
|
||||
.min(6, 'projectId is required')
|
||||
.max(94)
|
||||
.refine((v) => BQ_PROJECT_ID_RE.test(v), {
|
||||
message: 'projectId must match Google Cloud project ID rules',
|
||||
}),
|
||||
datasetId: z
|
||||
.string()
|
||||
.min(1, 'datasetId is required')
|
||||
.refine((v) => BQ_DATASET_RE.test(v), {
|
||||
message: 'datasetId may only contain letters, digits, and underscores (max 1024)',
|
||||
}),
|
||||
tableId: z
|
||||
.string()
|
||||
.min(1, 'tableId is required')
|
||||
.refine((v) => BQ_TABLE_RE.test(v), {
|
||||
message:
|
||||
'tableId may contain Unicode letters, marks, numbers, connectors, dashes, and spaces (max 1024)',
|
||||
})
|
||||
.refine((v) => Buffer.byteLength(v, 'utf8') <= 1024, {
|
||||
message: 'tableId must be at most 1024 bytes (UTF-8)',
|
||||
}),
|
||||
})
|
||||
|
||||
const bigqueryCredentialsBodySchema = z.object({
|
||||
serviceAccountJson: z.string().min(1, 'serviceAccountJson is required'),
|
||||
})
|
||||
|
||||
const snowflakeConfigBodySchema = z.object({
|
||||
account: z
|
||||
.string()
|
||||
.min(3, 'account is required')
|
||||
.max(256)
|
||||
.refine((v) => SNOWFLAKE_ACCOUNT_ORG_RE.test(v) || SNOWFLAKE_ACCOUNT_LOCATOR_RE.test(v), {
|
||||
message:
|
||||
'account must be a Snowflake org-account identifier (orgname-accountname) or legacy locator (locator[.region[.cloud]])',
|
||||
}),
|
||||
user: z.string().min(1, 'user is required').regex(SNOWFLAKE_IDENTIFIER_RE, {
|
||||
message: 'user must be a valid Snowflake identifier',
|
||||
}),
|
||||
warehouse: z.string().min(1).regex(SNOWFLAKE_IDENTIFIER_RE, {
|
||||
message: 'warehouse must be a valid Snowflake identifier',
|
||||
}),
|
||||
database: z.string().min(1).regex(SNOWFLAKE_IDENTIFIER_RE, {
|
||||
message: 'database must be a valid Snowflake identifier',
|
||||
}),
|
||||
schema: z.string().min(1).regex(SNOWFLAKE_IDENTIFIER_RE, {
|
||||
message: 'schema must be a valid Snowflake identifier',
|
||||
}),
|
||||
table: z.string().min(1).regex(SNOWFLAKE_IDENTIFIER_RE, {
|
||||
message: 'table must be a valid Snowflake identifier',
|
||||
}),
|
||||
column: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(SNOWFLAKE_IDENTIFIER_RE, { message: 'column must be a valid Snowflake identifier' })
|
||||
.optional(),
|
||||
role: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(SNOWFLAKE_IDENTIFIER_RE, { message: 'role must be a valid Snowflake identifier' })
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const snowflakeCredentialsBodySchema = z.object({
|
||||
privateKey: z.string().min(1, 'privateKey is required'),
|
||||
})
|
||||
|
||||
const webhookConfigBodySchema = z.object({
|
||||
url: z
|
||||
.string()
|
||||
.url('url must be a valid URL')
|
||||
.max(2048, 'url must be at most 2048 characters')
|
||||
.refine((value) => validateExternalUrl(value, 'url').isValid, {
|
||||
message: 'url must be HTTPS and not point at a private, loopback, or metadata address',
|
||||
}),
|
||||
signatureHeader: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(128)
|
||||
.refine((value) => !RESERVED_WEBHOOK_SIGNATURE_HEADER_NAMES.has(value.toLowerCase()), {
|
||||
message: 'signatureHeader cannot reuse a reserved Sim header name',
|
||||
})
|
||||
.refine((value) => /^[A-Za-z0-9\-_]+$/.test(value) && !/[\r\n\0]/.test(value), {
|
||||
message: 'signatureHeader must contain only letters, digits, hyphens, and underscores',
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const webhookCredentialsBodySchema = z.object({
|
||||
signingSecret: z
|
||||
.string()
|
||||
.min(32, 'signingSecret must be at least 32 characters')
|
||||
.max(512, 'signingSecret must be at most 512 characters'),
|
||||
bearerToken: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(4096, 'bearerToken must be at most 4096 characters')
|
||||
.refine((value) => !/[\r\n\0]/.test(value), {
|
||||
message: 'bearerToken cannot contain CR, LF, or NUL characters',
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Discriminated body shape used by both create and update. Each destination
|
||||
* variant carries its own typed `destinationConfig` and optional
|
||||
* `destinationCredentials`. On update, omitting `destinationCredentials`
|
||||
* leaves the encrypted blob in place.
|
||||
*/
|
||||
export const dataDrainDestinationBodySchema = z.discriminatedUnion('destinationType', [
|
||||
z.object({
|
||||
destinationType: z.literal('s3'),
|
||||
destinationConfig: s3ConfigBodySchema,
|
||||
destinationCredentials: s3CredentialsBodySchema.optional(),
|
||||
}),
|
||||
z.object({
|
||||
destinationType: z.literal('gcs'),
|
||||
destinationConfig: gcsConfigBodySchema,
|
||||
destinationCredentials: gcsCredentialsBodySchema.optional(),
|
||||
}),
|
||||
z.object({
|
||||
destinationType: z.literal('azure_blob'),
|
||||
destinationConfig: azureBlobConfigBodySchema,
|
||||
destinationCredentials: azureBlobCredentialsBodySchema.optional(),
|
||||
}),
|
||||
z.object({
|
||||
destinationType: z.literal('datadog'),
|
||||
destinationConfig: datadogConfigBodySchema,
|
||||
destinationCredentials: datadogCredentialsBodySchema.optional(),
|
||||
}),
|
||||
z.object({
|
||||
destinationType: z.literal('bigquery'),
|
||||
destinationConfig: bigqueryConfigBodySchema,
|
||||
destinationCredentials: bigqueryCredentialsBodySchema.optional(),
|
||||
}),
|
||||
z.object({
|
||||
destinationType: z.literal('snowflake'),
|
||||
destinationConfig: snowflakeConfigBodySchema,
|
||||
destinationCredentials: snowflakeCredentialsBodySchema.optional(),
|
||||
}),
|
||||
z.object({
|
||||
destinationType: z.literal('webhook'),
|
||||
destinationConfig: webhookConfigBodySchema,
|
||||
destinationCredentials: webhookCredentialsBodySchema.optional(),
|
||||
}),
|
||||
])
|
||||
|
||||
const drainCommonBodyFieldsSchema = z.object({
|
||||
name: drainNameSchema,
|
||||
source: dataDrainSourceSchema,
|
||||
scheduleCadence: dataDrainCadenceSchema,
|
||||
enabled: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const createDataDrainBodySchema = z.intersection(
|
||||
drainCommonBodyFieldsSchema,
|
||||
dataDrainDestinationBodySchema
|
||||
)
|
||||
|
||||
/**
|
||||
* Update bodies are partial — every field is optional. We deliberately don't
|
||||
* use a discriminated union here: clients sending `{ enabled: false }` should
|
||||
* not be forced to also send `destinationType`. The route validates the
|
||||
* destination payloads against the typed `configSchema` / `credentialsSchema`
|
||||
* for the existing drain's destination type before persisting, so the
|
||||
* structural shape is still enforced — just at the route layer rather than at
|
||||
* the contract boundary.
|
||||
*/
|
||||
export const updateDataDrainBodySchema = drainCommonBodyFieldsSchema.partial().extend({
|
||||
destinationType: dataDrainDestinationTypeSchema.optional(),
|
||||
destinationConfig: z.record(z.string(), z.unknown()).optional(),
|
||||
destinationCredentials: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
|
||||
const drainDestinationResponseSchema = z.discriminatedUnion('destinationType', [
|
||||
z.object({
|
||||
destinationType: z.literal('s3'),
|
||||
destinationConfig: s3ConfigBodySchema,
|
||||
}),
|
||||
z.object({
|
||||
destinationType: z.literal('gcs'),
|
||||
destinationConfig: gcsConfigBodySchema,
|
||||
}),
|
||||
z.object({
|
||||
destinationType: z.literal('azure_blob'),
|
||||
destinationConfig: azureBlobConfigBodySchema,
|
||||
}),
|
||||
z.object({
|
||||
destinationType: z.literal('datadog'),
|
||||
destinationConfig: datadogConfigBodySchema,
|
||||
}),
|
||||
z.object({
|
||||
destinationType: z.literal('bigquery'),
|
||||
destinationConfig: bigqueryConfigBodySchema,
|
||||
}),
|
||||
z.object({
|
||||
destinationType: z.literal('snowflake'),
|
||||
destinationConfig: snowflakeConfigBodySchema,
|
||||
}),
|
||||
z.object({
|
||||
destinationType: z.literal('webhook'),
|
||||
destinationConfig: webhookConfigBodySchema,
|
||||
}),
|
||||
])
|
||||
|
||||
const drainCommonResponseFieldsSchema = z.object({
|
||||
id: z.string(),
|
||||
organizationId: z.string(),
|
||||
name: z.string(),
|
||||
source: dataDrainSourceSchema,
|
||||
scheduleCadence: dataDrainCadenceSchema,
|
||||
enabled: z.boolean(),
|
||||
cursor: z.string().nullable(),
|
||||
lastRunAt: z.string().nullable(),
|
||||
lastSuccessAt: z.string().nullable(),
|
||||
createdBy: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
|
||||
export const dataDrainSchema = z.intersection(
|
||||
drainCommonResponseFieldsSchema,
|
||||
drainDestinationResponseSchema
|
||||
)
|
||||
|
||||
export type DataDrain = z.output<typeof dataDrainSchema>
|
||||
export type CreateDataDrainBody = z.input<typeof createDataDrainBodySchema>
|
||||
export type UpdateDataDrainBody = z.input<typeof updateDataDrainBodySchema>
|
||||
|
||||
export const dataDrainListResponseSchema = z.object({
|
||||
drains: z.array(dataDrainSchema),
|
||||
})
|
||||
|
||||
export const dataDrainResponseSchema = z.object({
|
||||
drain: dataDrainSchema,
|
||||
})
|
||||
|
||||
export const dataDrainRunSchema = z.object({
|
||||
id: z.string(),
|
||||
drainId: z.string(),
|
||||
status: dataDrainRunStatusSchema,
|
||||
trigger: dataDrainRunTriggerSchema,
|
||||
startedAt: z.string(),
|
||||
finishedAt: z.string().nullable(),
|
||||
rowsExported: z.number().int(),
|
||||
bytesWritten: z.number().int(),
|
||||
cursorBefore: z.string().nullable(),
|
||||
cursorAfter: z.string().nullable(),
|
||||
error: z.string().nullable(),
|
||||
locators: z.array(z.string()),
|
||||
})
|
||||
|
||||
export type DataDrainRun = z.output<typeof dataDrainRunSchema>
|
||||
|
||||
export const dataDrainRunListResponseSchema = z.object({
|
||||
runs: z.array(dataDrainRunSchema),
|
||||
})
|
||||
|
||||
export const runDataDrainResponseSchema = z.object({
|
||||
jobId: z.string(),
|
||||
})
|
||||
|
||||
export const testDataDrainResponseSchema = z.object({
|
||||
ok: z.literal(true),
|
||||
})
|
||||
|
||||
export const listDataDrainsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/organizations/[id]/data-drains',
|
||||
params: dataDrainOrgParamsSchema,
|
||||
response: { mode: 'json', schema: dataDrainListResponseSchema },
|
||||
})
|
||||
|
||||
export const createDataDrainContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/organizations/[id]/data-drains',
|
||||
params: dataDrainOrgParamsSchema,
|
||||
body: createDataDrainBodySchema,
|
||||
response: { mode: 'json', schema: dataDrainResponseSchema },
|
||||
})
|
||||
|
||||
export const getDataDrainContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/organizations/[id]/data-drains/[drainId]',
|
||||
params: dataDrainParamsSchema,
|
||||
response: { mode: 'json', schema: dataDrainResponseSchema },
|
||||
})
|
||||
|
||||
export const updateDataDrainContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/organizations/[id]/data-drains/[drainId]',
|
||||
params: dataDrainParamsSchema,
|
||||
body: updateDataDrainBodySchema,
|
||||
response: { mode: 'json', schema: dataDrainResponseSchema },
|
||||
})
|
||||
|
||||
export const deleteDataDrainContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/organizations/[id]/data-drains/[drainId]',
|
||||
params: dataDrainParamsSchema,
|
||||
response: { mode: 'json', schema: z.object({ success: z.literal(true) }) },
|
||||
})
|
||||
|
||||
export const runDataDrainContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/organizations/[id]/data-drains/[drainId]/run',
|
||||
params: dataDrainParamsSchema,
|
||||
response: { mode: 'json', schema: runDataDrainResponseSchema },
|
||||
})
|
||||
|
||||
export const testDataDrainContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/organizations/[id]/data-drains/[drainId]/test',
|
||||
params: dataDrainParamsSchema,
|
||||
response: { mode: 'json', schema: testDataDrainResponseSchema },
|
||||
})
|
||||
|
||||
export const listDataDrainRunsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/organizations/[id]/data-drains/[drainId]/runs',
|
||||
params: dataDrainParamsSchema,
|
||||
query: z
|
||||
.object({
|
||||
limit: z
|
||||
.preprocess(
|
||||
(v) => (typeof v === 'string' ? Number.parseInt(v, 10) : v),
|
||||
z.number().int().min(1).max(200)
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
response: { mode: 'json', schema: dataDrainRunListResponseSchema },
|
||||
})
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { updateOrganizationDataRetentionBodySchema } from '@/lib/api/contracts/organization'
|
||||
import {
|
||||
piiRedactionRuleSchema,
|
||||
piiRedactionSettingsSchema,
|
||||
retentionOverridesSchema,
|
||||
} from '@/lib/api/contracts/primitives'
|
||||
|
||||
describe('retentionOverridesSchema', () => {
|
||||
it('accepts an override that overrides one field and inherits the rest', () => {
|
||||
const result = retentionOverridesSchema.safeParse([
|
||||
{ workspaceId: 'ws-1', logRetentionHours: 168 },
|
||||
])
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts null (forever) for a field', () => {
|
||||
const result = retentionOverridesSchema.safeParse([
|
||||
{ workspaceId: 'ws-1', logRetentionHours: null },
|
||||
])
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects two overrides for the same workspace', () => {
|
||||
const result = retentionOverridesSchema.safeParse([
|
||||
{ workspaceId: 'ws-1', logRetentionHours: 168 },
|
||||
{ workspaceId: 'ws-1', softDeleteRetentionHours: 720 },
|
||||
])
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('allows distinct workspaces', () => {
|
||||
const result = retentionOverridesSchema.safeParse([
|
||||
{ workspaceId: 'ws-1', logRetentionHours: 168 },
|
||||
{ workspaceId: 'ws-2', logRetentionHours: 720 },
|
||||
])
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects hours below the 24h minimum and above the ~5y maximum', () => {
|
||||
expect(
|
||||
retentionOverridesSchema.safeParse([{ workspaceId: 'ws-1', logRetentionHours: 1 }]).success
|
||||
).toBe(false)
|
||||
expect(
|
||||
retentionOverridesSchema.safeParse([{ workspaceId: 'ws-1', logRetentionHours: 100000 }])
|
||||
.success
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects an empty workspaceId', () => {
|
||||
expect(
|
||||
retentionOverridesSchema.safeParse([{ workspaceId: '', logRetentionHours: 168 }]).success
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateOrganizationDataRetentionBodySchema', () => {
|
||||
it('accepts retentionOverrides alongside the org hours', () => {
|
||||
const result = updateOrganizationDataRetentionBodySchema.safeParse({
|
||||
logRetentionHours: 720,
|
||||
retentionOverrides: [{ workspaceId: 'ws-1', logRetentionHours: 168 }],
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts a body with no retentionOverrides (field is optional)', () => {
|
||||
const result = updateOrganizationDataRetentionBodySchema.safeParse({ logRetentionHours: 720 })
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('piiRedactionRuleSchema', () => {
|
||||
const stage = (enabled: boolean, entityTypes: string[], language?: string) => ({
|
||||
enabled,
|
||||
entityTypes,
|
||||
...(language ? { language } : {}),
|
||||
})
|
||||
|
||||
it('accepts a legacy flat rule (entityTypes only)', () => {
|
||||
const result = piiRedactionRuleSchema.safeParse({
|
||||
id: 'r-1',
|
||||
workspaceId: null,
|
||||
entityTypes: ['EMAIL_ADDRESS'],
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts a per-stage rule', () => {
|
||||
const result = piiRedactionRuleSchema.safeParse({
|
||||
id: 'r-1',
|
||||
workspaceId: 'ws-1',
|
||||
stages: {
|
||||
input: stage(true, ['PERSON'], 'es'),
|
||||
blockOutputs: stage(false, []),
|
||||
logs: stage(true, ['US_SSN']),
|
||||
},
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects a rule with neither stages nor entityTypes', () => {
|
||||
const result = piiRedactionRuleSchema.safeParse({ id: 'r-1', workspaceId: null })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects an enabled stage with no entity types (redact-all is not expressible)', () => {
|
||||
const result = piiRedactionRuleSchema.safeParse({
|
||||
id: 'r-1',
|
||||
workspaceId: null,
|
||||
stages: {
|
||||
input: stage(true, []),
|
||||
blockOutputs: stage(false, []),
|
||||
logs: stage(false, []),
|
||||
},
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts a disabled stage with no entity types (off)', () => {
|
||||
const result = piiRedactionRuleSchema.safeParse({
|
||||
id: 'r-1',
|
||||
workspaceId: null,
|
||||
stages: {
|
||||
input: stage(false, []),
|
||||
blockOutputs: stage(false, []),
|
||||
logs: stage(true, ['PERSON']),
|
||||
},
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects an unsupported stage language', () => {
|
||||
const result = piiRedactionRuleSchema.safeParse({
|
||||
id: 'r-1',
|
||||
workspaceId: null,
|
||||
stages: {
|
||||
input: stage(true, ['PERSON'], 'de'),
|
||||
blockOutputs: stage(false, []),
|
||||
logs: stage(false, []),
|
||||
},
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('enforces one rule per scope (uniqueness refine still applies)', () => {
|
||||
const result = piiRedactionSettingsSchema.safeParse({
|
||||
rules: [
|
||||
{ id: 'r-1', workspaceId: 'ws-1', entityTypes: ['PERSON'] },
|
||||
{ id: 'r-2', workspaceId: 'ws-1', entityTypes: ['US_SSN'] },
|
||||
],
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import { isFreeEmailDomain } from '@/lib/messaging/email/free-email'
|
||||
import { NO_EMAIL_HEADER_CONTROL_CHARS_REGEX } from '@/lib/messaging/email/utils'
|
||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||
|
||||
export const DEMO_REQUEST_COMPANY_SIZE_VALUES = [
|
||||
'1_10',
|
||||
'11_50',
|
||||
'51_200',
|
||||
'201_500',
|
||||
'501_1000',
|
||||
'1001_10000',
|
||||
'10000_plus',
|
||||
] as const
|
||||
|
||||
export const DEMO_REQUEST_COMPANY_SIZE_OPTIONS = [
|
||||
{ value: '1_10', label: '1–10' },
|
||||
{ value: '11_50', label: '11–50' },
|
||||
{ value: '51_200', label: '51–200' },
|
||||
{ value: '201_500', label: '201–500' },
|
||||
{ value: '501_1000', label: '501–1,000' },
|
||||
{ value: '1001_10000', label: '1,001–10,000' },
|
||||
{ value: '10000_plus', label: '10,000+' },
|
||||
] as const
|
||||
|
||||
export const demoRequestSchema = z.object({
|
||||
firstName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'First name is required')
|
||||
.max(100)
|
||||
.regex(NO_EMAIL_HEADER_CONTROL_CHARS_REGEX, 'Invalid characters'),
|
||||
lastName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Last name is required')
|
||||
.max(100)
|
||||
.regex(NO_EMAIL_HEADER_CONTROL_CHARS_REGEX, 'Invalid characters'),
|
||||
companyEmail: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Company email is required')
|
||||
.max(320)
|
||||
.transform((value) => value.toLowerCase())
|
||||
.refine((value) => quickValidateEmail(value).isValid, 'Enter a valid work email')
|
||||
.refine((value) => !isFreeEmailDomain(value), 'Please use your work email address'),
|
||||
phoneNumber: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(50, 'Phone number must be 50 characters or less')
|
||||
.optional()
|
||||
.transform((value) => (value && value.length > 0 ? value : undefined)),
|
||||
companySize: z.enum(DEMO_REQUEST_COMPANY_SIZE_VALUES, {
|
||||
error: 'Please select company size',
|
||||
}),
|
||||
details: z.string().trim().min(1, 'Details are required').max(2000),
|
||||
})
|
||||
|
||||
export type DemoRequestPayload = z.infer<typeof demoRequestSchema>
|
||||
export type DemoRequestBody = z.input<typeof demoRequestSchema>
|
||||
|
||||
export function getDemoRequestCompanySizeLabel(value: DemoRequestPayload['companySize']): string {
|
||||
return DEMO_REQUEST_COMPANY_SIZE_OPTIONS.find((option) => option.value === value)?.label ?? value
|
||||
}
|
||||
|
||||
export const demoRequestResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
message: z.string(),
|
||||
})
|
||||
|
||||
export type DemoRequestResult = z.output<typeof demoRequestResponseSchema>
|
||||
|
||||
export const submitDemoRequestContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/demo-requests',
|
||||
body: demoRequestSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: demoRequestResponseSchema,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,307 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import { workflowIdParamsSchema } from '@/lib/api/contracts/workflows'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const deployedWorkflowStateSchema = z.custom<WorkflowState>(
|
||||
(value) => typeof value === 'object' && value !== null,
|
||||
'Expected workflow state'
|
||||
)
|
||||
|
||||
export const deploymentVersionParamsSchema = z.object({
|
||||
id: z.string().min(1, 'Invalid workflow ID'),
|
||||
version: z.coerce.number().int().positive(),
|
||||
})
|
||||
|
||||
export const deploymentVersionOrActiveParamsSchema = z.object({
|
||||
id: z.string().min(1, 'Invalid workflow ID'),
|
||||
version: z.union([z.number().int().positive(), z.literal('active')]),
|
||||
})
|
||||
|
||||
export const deploymentVersionRouteParamsSchema = z.object({
|
||||
id: z.string().min(1, 'Invalid workflow ID'),
|
||||
version: z.string().min(1, 'Invalid version'),
|
||||
})
|
||||
|
||||
export const updatePublicApiBodySchema = z.object({
|
||||
isPublicApi: z.boolean(),
|
||||
})
|
||||
|
||||
export type UpdatePublicApiBody = z.input<typeof updatePublicApiBodySchema>
|
||||
|
||||
export const deploymentVersionMetadataFieldsSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Name cannot be empty')
|
||||
.max(100, 'Name must be 100 characters or less')
|
||||
.optional(),
|
||||
description: z.string().trim().max(50_000, 'Description is too long').nullable().optional(),
|
||||
})
|
||||
export const updateDeploymentVersionMetadataBodySchema =
|
||||
deploymentVersionMetadataFieldsSchema.refine(
|
||||
(data) => data.name !== undefined || data.description !== undefined,
|
||||
{
|
||||
message: 'At least one of name or description must be provided',
|
||||
}
|
||||
)
|
||||
|
||||
export type UpdateDeploymentVersionMetadataBody = z.input<
|
||||
typeof updateDeploymentVersionMetadataBodySchema
|
||||
>
|
||||
|
||||
export const activateDeploymentVersionBodySchema = z.object({
|
||||
isActive: z.literal(true),
|
||||
})
|
||||
|
||||
export type ActivateDeploymentVersionBody = z.input<typeof activateDeploymentVersionBodySchema>
|
||||
|
||||
export const deploymentVersionPatchBodySchema = deploymentVersionMetadataFieldsSchema
|
||||
.extend({
|
||||
isActive: z.literal(true).optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => data.name !== undefined || data.description !== undefined || data.isActive === true,
|
||||
{
|
||||
message: 'At least one of name, description, or isActive must be provided',
|
||||
}
|
||||
)
|
||||
|
||||
export const deploymentInfoResponseSchema = z.object({
|
||||
isDeployed: z.boolean(),
|
||||
deployedAt: z.string().nullable().optional(),
|
||||
apiKey: z.string().nullable().optional(),
|
||||
needsRedeployment: z.boolean().optional(),
|
||||
isPublicApi: z.boolean().optional(),
|
||||
warnings: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export type DeploymentInfoResponse = z.output<typeof deploymentInfoResponseSchema>
|
||||
export type DeployWorkflowResponse = DeploymentInfoResponse
|
||||
export type UndeployWorkflowResponse = DeploymentInfoResponse
|
||||
|
||||
export const deploymentVersionSchema = z.object({
|
||||
id: z.string(),
|
||||
version: z.number(),
|
||||
name: z.string().nullable().optional(),
|
||||
description: z.string().nullable().optional(),
|
||||
isActive: z.boolean(),
|
||||
createdAt: z.string(),
|
||||
createdBy: z.string().nullable().optional(),
|
||||
deployedBy: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
export type DeploymentVersion = z.output<typeof deploymentVersionSchema>
|
||||
|
||||
export const deploymentVersionsResponseSchema = z.object({
|
||||
versions: z.array(deploymentVersionSchema),
|
||||
})
|
||||
|
||||
export type DeploymentVersionsResponse = z.output<typeof deploymentVersionsResponseSchema>
|
||||
|
||||
export const chatDeploymentStatusSchema = z.object({
|
||||
isDeployed: z.boolean(),
|
||||
deployment: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
identifier: z.string(),
|
||||
})
|
||||
.passthrough()
|
||||
.nullable(),
|
||||
})
|
||||
|
||||
export type ChatDeploymentStatus = z.output<typeof chatDeploymentStatusSchema>
|
||||
|
||||
export const chatDetailSchema = z.object({
|
||||
id: z.string(),
|
||||
identifier: z.string(),
|
||||
title: z.string(),
|
||||
description: z.preprocess((value) => value ?? '', z.string()),
|
||||
authType: z.enum(['public', 'password', 'email', 'sso']),
|
||||
allowedEmails: z.preprocess((value) => value ?? [], z.array(z.string())),
|
||||
outputConfigs: z.preprocess(
|
||||
(value) => value ?? [],
|
||||
z.array(
|
||||
z.object({
|
||||
blockId: z.string(),
|
||||
path: z.string(),
|
||||
})
|
||||
)
|
||||
),
|
||||
customizations: z.preprocess(
|
||||
(value) => value ?? undefined,
|
||||
z
|
||||
.object({
|
||||
welcomeMessage: z.string().optional(),
|
||||
imageUrl: z.string().optional(),
|
||||
primaryColor: z.string().optional(),
|
||||
})
|
||||
.optional()
|
||||
),
|
||||
isActive: z.boolean(),
|
||||
chatUrl: z.string(),
|
||||
hasPassword: z.boolean(),
|
||||
})
|
||||
|
||||
export type ChatDetail = z.output<typeof chatDetailSchema>
|
||||
|
||||
export const updatePublicApiResponseSchema = z.object({
|
||||
isPublicApi: z.boolean(),
|
||||
})
|
||||
|
||||
export type UpdatePublicApiResponse = z.output<typeof updatePublicApiResponseSchema>
|
||||
|
||||
export const deployedWorkflowStateResponseSchema = z.object({
|
||||
deployedState: deployedWorkflowStateSchema.nullable(),
|
||||
})
|
||||
|
||||
export type DeployedWorkflowStateResponse = z.output<typeof deployedWorkflowStateResponseSchema>
|
||||
|
||||
export const updateDeploymentVersionMetadataResponseSchema = z.object({
|
||||
name: z.string().nullable(),
|
||||
description: z.string().nullable(),
|
||||
})
|
||||
|
||||
export type UpdateDeploymentVersionMetadataResponse = z.output<
|
||||
typeof updateDeploymentVersionMetadataResponseSchema
|
||||
>
|
||||
|
||||
export const activateDeploymentVersionResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
deployedAt: z.string(),
|
||||
warnings: z.array(z.string()).optional(),
|
||||
name: z.string().nullable().optional(),
|
||||
description: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
export type ActivateDeploymentVersionResponse = z.output<
|
||||
typeof activateDeploymentVersionResponseSchema
|
||||
>
|
||||
|
||||
export const getDeploymentInfoContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/workflows/[id]/deploy',
|
||||
params: workflowIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: deploymentInfoResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const deployWorkflowContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/workflows/[id]/deploy',
|
||||
params: workflowIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: deploymentInfoResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const undeployWorkflowContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/workflows/[id]/deploy',
|
||||
params: workflowIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: deploymentInfoResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const updatePublicApiContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/workflows/[id]/deploy',
|
||||
params: workflowIdParamsSchema,
|
||||
body: updatePublicApiBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: updatePublicApiResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getDeployedWorkflowStateContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/workflows/[id]/deployed',
|
||||
params: workflowIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: deployedWorkflowStateResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const listDeploymentVersionsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/workflows/[id]/deployments',
|
||||
params: workflowIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: deploymentVersionsResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getDeploymentVersionStateContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/workflows/[id]/deployments/[version]',
|
||||
params: deploymentVersionParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
deployedState: deployedWorkflowStateSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateDeploymentVersionMetadataContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/workflows/[id]/deployments/[version]',
|
||||
params: deploymentVersionParamsSchema,
|
||||
body: deploymentVersionPatchBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: updateDeploymentVersionMetadataResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const activateDeploymentVersionContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/workflows/[id]/deployments/[version]',
|
||||
params: deploymentVersionParamsSchema,
|
||||
body: activateDeploymentVersionBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: activateDeploymentVersionResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const revertToDeploymentVersionContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/workflows/[id]/deployments/[version]/revert',
|
||||
params: deploymentVersionOrActiveParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
message: z.string(),
|
||||
lastSaved: z.number(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const getChatDeploymentStatusContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/workflows/[id]/chat/status',
|
||||
params: workflowIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: chatDeploymentStatusSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getChatDetailContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/chat/manage/[id]',
|
||||
params: workflowIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: chatDetailSchema,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const environmentVariableSchema = z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
|
||||
export const environmentVariablesSchema = z.record(z.string(), z.string())
|
||||
|
||||
export const personalEnvironmentDataSchema = z.record(z.string(), environmentVariableSchema)
|
||||
|
||||
export const workspaceEnvironmentDataSchema = z.object({
|
||||
workspace: environmentVariablesSchema.default({}),
|
||||
personal: environmentVariablesSchema.default({}),
|
||||
conflicts: z.array(z.string()).default([]),
|
||||
})
|
||||
|
||||
export const workspaceEnvironmentParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const savePersonalEnvironmentBodySchema = z.object({
|
||||
variables: environmentVariablesSchema,
|
||||
})
|
||||
|
||||
export const removeWorkspaceEnvironmentBodySchema = z.object({
|
||||
keys: z.array(z.string()).min(1),
|
||||
})
|
||||
|
||||
const successResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
})
|
||||
|
||||
export const getPersonalEnvironmentContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/environment',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
data: personalEnvironmentDataSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const savePersonalEnvironmentContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/environment',
|
||||
body: savePersonalEnvironmentBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getWorkspaceEnvironmentContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/workspaces/[id]/environment',
|
||||
params: workspaceEnvironmentParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
data: workspaceEnvironmentDataSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const upsertWorkspaceEnvironmentContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/workspaces/[id]/environment',
|
||||
params: workspaceEnvironmentParamsSchema,
|
||||
body: savePersonalEnvironmentBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const removeWorkspaceEnvironmentContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/workspaces/[id]/environment',
|
||||
params: workspaceEnvironmentParamsSchema,
|
||||
body: removeWorkspaceEnvironmentBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
isLargeValueStorageKey,
|
||||
LARGE_VALUE_KINDS,
|
||||
LARGE_VALUE_REF_MARKER,
|
||||
LARGE_VALUE_REF_VERSION,
|
||||
} from '@/lib/execution/payloads/large-value-ref'
|
||||
|
||||
export const largeValueRefSchema = z
|
||||
.object({
|
||||
[LARGE_VALUE_REF_MARKER]: z.literal(true),
|
||||
version: z.literal(LARGE_VALUE_REF_VERSION),
|
||||
id: z.string().regex(/^lv_[A-Za-z0-9_-]{12}$/, 'Invalid large value reference ID'),
|
||||
kind: z.enum(LARGE_VALUE_KINDS),
|
||||
size: z.number().int().positive(),
|
||||
key: z.string().optional(),
|
||||
executionId: z.string().optional(),
|
||||
preview: z.unknown().optional(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((value, ctx) => {
|
||||
if (value.key && !isLargeValueStorageKey(value.key, value.id, value.executionId)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['key'],
|
||||
message: 'Large value reference key must point to execution-scoped server storage',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export type LargeValueRefResponse = z.output<typeof largeValueRefSchema>
|
||||
@@ -0,0 +1,88 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const fileUploadTypeSchema = z.enum([
|
||||
'knowledge-base',
|
||||
'chat',
|
||||
'copilot',
|
||||
'profile-pictures',
|
||||
])
|
||||
|
||||
export const fileUploadTypeQuerySchema = z.object({
|
||||
type: fileUploadTypeSchema,
|
||||
})
|
||||
|
||||
export const presignedUploadBodySchema = z
|
||||
.object({
|
||||
fileName: z.string().optional(),
|
||||
contentType: z.string().optional(),
|
||||
fileSize: z.number().optional(),
|
||||
userId: z.string().optional(),
|
||||
chatId: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const batchPresignedUploadBodySchema = z
|
||||
.object({
|
||||
files: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
fileName: z.string().optional(),
|
||||
contentType: z.string().optional(),
|
||||
fileSize: z.number().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const presignedFileInfoSchema = z
|
||||
.object({
|
||||
path: z.string(),
|
||||
key: z.string(),
|
||||
name: z.string(),
|
||||
size: z.number(),
|
||||
type: z.string(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const presignedUploadResponseSchema = z
|
||||
.object({
|
||||
fileName: z.string(),
|
||||
presignedUrl: z.string(),
|
||||
fileInfo: presignedFileInfoSchema,
|
||||
uploadHeaders: z.record(z.string(), z.string()).optional(),
|
||||
directUploadSupported: z.boolean(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const batchPresignedUploadResponseSchema = z
|
||||
.object({
|
||||
files: z.array(presignedUploadResponseSchema),
|
||||
directUploadSupported: z.boolean(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const createPresignedUploadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/presigned',
|
||||
query: fileUploadTypeQuerySchema,
|
||||
body: presignedUploadBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: presignedUploadResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const createBatchPresignedUploadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/presigned/batch',
|
||||
query: fileUploadTypeQuerySchema,
|
||||
body: batchPresignedUploadBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: batchPresignedUploadResponseSchema,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,162 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const folderScopeSchema = z.enum(['active', 'archived'])
|
||||
|
||||
export const folderSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
userId: z.string(),
|
||||
workspaceId: z.string(),
|
||||
parentId: z.string().nullable(),
|
||||
color: z.string().nullable(),
|
||||
isExpanded: z.boolean(),
|
||||
locked: z.boolean(),
|
||||
sortOrder: z.number(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
archivedAt: z.string().nullable(),
|
||||
})
|
||||
|
||||
export type FolderApi = z.output<typeof folderSchema>
|
||||
|
||||
export const listFoldersQuerySchema = z.object({
|
||||
workspaceId: z.string({ error: 'Workspace ID is required' }).min(1, 'Workspace ID is required'),
|
||||
scope: folderScopeSchema.default('active'),
|
||||
})
|
||||
|
||||
export const createFolderBodySchema = z.object({
|
||||
id: z.string().uuid().optional(),
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
||||
/** Mirrors `updateFolderBodySchema.parentId` so explicit `null` (root folder) is accepted on create. */
|
||||
parentId: z.string().nullable().optional(),
|
||||
color: z.string().optional(),
|
||||
sortOrder: z.number().int().optional(),
|
||||
})
|
||||
|
||||
export const folderIdParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const updateFolderBodySchema = z.object({
|
||||
name: z.string().optional(),
|
||||
color: z.string().optional(),
|
||||
isExpanded: z.boolean().optional(),
|
||||
locked: z.boolean().optional(),
|
||||
parentId: z.string().nullable().optional(),
|
||||
sortOrder: z.number().int().min(0).optional(),
|
||||
})
|
||||
|
||||
export const restoreFolderBodySchema = z.object({
|
||||
workspaceId: z.string({ error: 'Workspace ID is required' }).min(1, 'Workspace ID is required'),
|
||||
})
|
||||
|
||||
export const duplicateFolderBodySchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
workspaceId: z.string().optional(),
|
||||
parentId: z.string().nullable().optional(),
|
||||
color: z.string().optional(),
|
||||
newId: z.string().uuid().optional(),
|
||||
})
|
||||
|
||||
export const reorderFoldersBodySchema = z.object({
|
||||
workspaceId: z.string(),
|
||||
updates: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
sortOrder: z.number().int().min(0),
|
||||
parentId: z.string().nullable().optional(),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
export const listFoldersContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/folders',
|
||||
query: listFoldersQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
folders: z.array(folderSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const createFolderContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/folders',
|
||||
body: createFolderBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
folder: folderSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateFolderContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/folders/[id]',
|
||||
params: folderIdParamsSchema,
|
||||
body: updateFolderBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
folder: folderSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteFolderContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/folders/[id]',
|
||||
params: folderIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
deletedItems: z.unknown(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const restoreFolderContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/folders/[id]/restore',
|
||||
params: folderIdParamsSchema,
|
||||
body: restoreFolderBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
restoredItems: z.unknown(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const duplicateFolderContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/folders/[id]/duplicate',
|
||||
params: folderIdParamsSchema,
|
||||
body: duplicateFolderBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
folder: folderSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const reorderFoldersContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/folders/reorder',
|
||||
body: reorderFoldersBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
updated: z.number().int(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,214 @@
|
||||
import { z } from 'zod'
|
||||
import { unknownRecordSchema } from '@/lib/api/contracts/primitives'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import { DEFAULT_CODE_LANGUAGE } from '@/lib/execution/languages'
|
||||
export const guardrailsValidateContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/guardrails/validate',
|
||||
body: z.object({
|
||||
validationType: z.string().optional(),
|
||||
input: z.unknown().optional(),
|
||||
regex: z.string().optional(),
|
||||
knowledgeBaseId: z.string().optional(),
|
||||
threshold: z.string().optional(),
|
||||
topK: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
apiKey: z.string().optional(),
|
||||
azureEndpoint: z.string().optional(),
|
||||
azureApiVersion: z.string().optional(),
|
||||
vertexProject: z.string().optional(),
|
||||
vertexLocation: z.string().optional(),
|
||||
vertexCredential: z.string().optional(),
|
||||
bedrockAccessKeyId: z.string().optional(),
|
||||
bedrockSecretKey: z.string().optional(),
|
||||
bedrockRegion: z.string().optional(),
|
||||
workflowId: z.string().optional(),
|
||||
piiEntityTypes: z.array(z.string()).optional(),
|
||||
piiMode: z.string().optional(),
|
||||
piiLanguage: z.string().optional(),
|
||||
}),
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.boolean(),
|
||||
output: z.object({
|
||||
passed: z.boolean(),
|
||||
validationType: z.string(),
|
||||
input: z.unknown().optional(),
|
||||
error: z.string().optional(),
|
||||
score: z.number().optional(),
|
||||
reasoning: z.string().optional(),
|
||||
detectedEntities: z.array(z.unknown()).optional(),
|
||||
maskedText: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const guardrailsMaskBatchBodySchema = z.object({
|
||||
texts: z.array(z.string()).max(100_000),
|
||||
entityTypes: z.array(z.string().min(1, 'Entity type cannot be empty')).max(200),
|
||||
language: z.string().min(1).max(20).optional(),
|
||||
})
|
||||
|
||||
const guardrailsMaskBatchResponseSchema = z.object({
|
||||
masked: z.array(z.string()),
|
||||
})
|
||||
|
||||
/**
|
||||
* Internal batch PII masking. Called server-to-server (internal JWT) from the
|
||||
* log-redaction persist path so Presidio always runs in the app container,
|
||||
* including for async executions that persist inside the trigger.dev runtime.
|
||||
*/
|
||||
export const guardrailsMaskBatchContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/guardrails/mask-batch',
|
||||
body: guardrailsMaskBatchBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: guardrailsMaskBatchResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export type GuardrailsMaskBatchBody = z.input<typeof guardrailsMaskBatchBodySchema>
|
||||
export type GuardrailsMaskBatchResult = z.output<typeof guardrailsMaskBatchResponseSchema>
|
||||
|
||||
const chatMessageSchema = z.object({
|
||||
role: z.enum(['user', 'assistant', 'system']),
|
||||
content: z.string(),
|
||||
})
|
||||
|
||||
const wandGenerateBodySchema = z.object({
|
||||
prompt: z.string().min(1, 'Missing required field: prompt.'),
|
||||
systemPrompt: z.string().optional(),
|
||||
stream: z.boolean().optional().default(false),
|
||||
history: z.array(chatMessageSchema).optional().default([]),
|
||||
workflowId: z.string().optional(),
|
||||
/** Falls back here for per-member usage attribution when no workflowId is sent. */
|
||||
workspaceId: z.string().optional(),
|
||||
generationType: z.string().optional(),
|
||||
wandContext: unknownRecordSchema.optional().default({}),
|
||||
})
|
||||
|
||||
export const wandGenerateContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/wand',
|
||||
body: wandGenerateBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: unknownRecordSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const wandGenerateStreamContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/wand',
|
||||
body: wandGenerateBodySchema.extend({
|
||||
stream: z.literal(true),
|
||||
}),
|
||||
response: {
|
||||
mode: 'stream',
|
||||
},
|
||||
})
|
||||
|
||||
const functionFileInputSchema = z
|
||||
.object({
|
||||
path: z.string().min(1, 'Input file path is required'),
|
||||
sandboxPath: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
const functionDirectoryInputSchema = z
|
||||
.object({
|
||||
path: z.string().min(1, 'Input directory path is required'),
|
||||
sandboxPath: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
const functionTableInputSchema = z
|
||||
.object({
|
||||
path: z.string().optional(),
|
||||
tableId: z.string().optional(),
|
||||
sandboxPath: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
const functionOutputFileSchema = z
|
||||
.object({
|
||||
path: z.string().min(1, 'Output file path is required'),
|
||||
mode: z.enum(['create', 'overwrite']).default('create'),
|
||||
sandboxPath: z.string().optional(),
|
||||
format: z.enum(['json', 'csv', 'txt', 'md', 'html']).optional(),
|
||||
mimeType: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const functionExecuteContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/function/execute',
|
||||
body: z.object({
|
||||
code: z.string().min(1, 'Code is required'),
|
||||
sourceCode: z.string().optional(),
|
||||
params: unknownRecordSchema.optional().default({}),
|
||||
timeout: z.coerce.number().int().positive().optional(),
|
||||
language: z.string().optional().default(DEFAULT_CODE_LANGUAGE),
|
||||
title: z.string().optional(),
|
||||
outputPath: z.string().optional(),
|
||||
outputFormat: z.string().optional(),
|
||||
outputTable: z.string().optional(),
|
||||
outputMimeType: z.string().optional(),
|
||||
outputSandboxPath: z.string().optional(),
|
||||
overwriteFileId: z.string().optional(),
|
||||
inputs: z
|
||||
.object({
|
||||
files: z.array(functionFileInputSchema).optional(),
|
||||
directories: z.array(functionDirectoryInputSchema).optional(),
|
||||
tables: z.array(functionTableInputSchema).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
outputs: z
|
||||
.object({
|
||||
files: z.array(functionOutputFileSchema).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
envVars: z.record(z.string(), z.string()).optional().default({}),
|
||||
blockData: unknownRecordSchema.optional().default({}),
|
||||
blockNameMapping: z.record(z.string(), z.string()).optional().default({}),
|
||||
blockOutputSchemas: z.record(z.string(), unknownRecordSchema).optional().default({}),
|
||||
workflowVariables: unknownRecordSchema.optional().default({}),
|
||||
contextVariables: unknownRecordSchema.optional().default({}),
|
||||
workflowId: z.string().optional(),
|
||||
executionId: z.string().optional(),
|
||||
largeValueExecutionIds: z.array(z.string()).optional(),
|
||||
largeValueKeys: z.array(z.string()).optional(),
|
||||
fileKeys: z.array(z.string()).optional(),
|
||||
allowLargeValueWorkflowScope: z.boolean().optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
isCustomTool: z.boolean().optional().default(false),
|
||||
_sandboxFiles: z
|
||||
.array(
|
||||
z.union([
|
||||
z.object({
|
||||
type: z.literal('content').optional(),
|
||||
path: z.string(),
|
||||
content: z.string(),
|
||||
encoding: z.literal('base64').optional(),
|
||||
}),
|
||||
// Mounted by reference: the sandbox fetches `url` itself (no bytes through the web tier).
|
||||
z.object({
|
||||
type: z.literal('url'),
|
||||
path: z.string(),
|
||||
url: z.string(),
|
||||
}),
|
||||
])
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: unknownRecordSchema,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,186 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const inboxWorkspaceParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const inboxTaskStatusSchema = z.enum([
|
||||
'all',
|
||||
'received',
|
||||
'processing',
|
||||
'completed',
|
||||
'failed',
|
||||
'rejected',
|
||||
])
|
||||
|
||||
export const inboxConfigSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
address: z.string().nullable(),
|
||||
entitled: z.boolean(),
|
||||
taskStats: z.object({
|
||||
total: z.number(),
|
||||
completed: z.number(),
|
||||
processing: z.number(),
|
||||
failed: z.number(),
|
||||
}),
|
||||
})
|
||||
|
||||
export type InboxConfig = z.output<typeof inboxConfigSchema>
|
||||
export type InboxTaskStatus = z.output<typeof inboxTaskStatusSchema>
|
||||
|
||||
export const updateInboxConfigBodySchema = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
username: z.string().min(1).max(64).optional(),
|
||||
})
|
||||
|
||||
export const updateInboxConfigResponseSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
address: z.string().nullable(),
|
||||
providerId: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
export const inboxSenderSchema = z.object({
|
||||
id: z.string(),
|
||||
email: z.string(),
|
||||
label: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
})
|
||||
|
||||
export type InboxSender = z.output<typeof inboxSenderSchema>
|
||||
|
||||
export const inboxMemberSchema = z.object({
|
||||
email: z.string(),
|
||||
name: z.string().nullable(),
|
||||
isAutoAllowed: z.boolean(),
|
||||
})
|
||||
|
||||
export type InboxMember = z.output<typeof inboxMemberSchema>
|
||||
|
||||
export const inboxSendersResponseSchema = z.object({
|
||||
senders: z.array(inboxSenderSchema),
|
||||
workspaceMembers: z.array(inboxMemberSchema),
|
||||
})
|
||||
|
||||
export type InboxSendersResponseBody = z.output<typeof inboxSendersResponseSchema>
|
||||
|
||||
export const createInboxSenderBodySchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
label: z.string().max(100).optional(),
|
||||
})
|
||||
|
||||
export const deleteInboxSenderBodySchema = z.object({
|
||||
senderId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const inboxTasksQuerySchema = z.object({
|
||||
status: inboxTaskStatusSchema.optional(),
|
||||
cursor: z.string().optional(),
|
||||
limit: z.preprocess(
|
||||
(value) => (value === undefined || value === '' ? undefined : value),
|
||||
z.coerce
|
||||
.number()
|
||||
.optional()
|
||||
.transform((value) => (value === undefined ? undefined : Math.min(value, 50)))
|
||||
),
|
||||
})
|
||||
|
||||
export const inboxTaskSchema = z.object({
|
||||
id: z.string(),
|
||||
fromEmail: z.string(),
|
||||
fromName: z.string().nullable(),
|
||||
subject: z.string(),
|
||||
bodyPreview: z.string().nullable(),
|
||||
status: inboxTaskStatusSchema.exclude(['all']),
|
||||
hasAttachments: z.boolean(),
|
||||
resultSummary: z.string().nullable(),
|
||||
errorMessage: z.string().nullable(),
|
||||
rejectionReason: z.string().nullable(),
|
||||
chatId: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
completedAt: z.string().nullable(),
|
||||
})
|
||||
|
||||
export type InboxTask = z.output<typeof inboxTaskSchema>
|
||||
|
||||
export const inboxTasksResponseSchema = z.object({
|
||||
tasks: z.array(inboxTaskSchema),
|
||||
pagination: z.object({
|
||||
limit: z.number(),
|
||||
hasMore: z.boolean(),
|
||||
nextCursor: z.string().nullable(),
|
||||
}),
|
||||
})
|
||||
|
||||
export type InboxTasksResponseBody = z.output<typeof inboxTasksResponseSchema>
|
||||
|
||||
export const getInboxConfigContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/workspaces/[id]/inbox',
|
||||
params: inboxWorkspaceParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: inboxConfigSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateInboxConfigContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/workspaces/[id]/inbox',
|
||||
params: inboxWorkspaceParamsSchema,
|
||||
body: updateInboxConfigBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: updateInboxConfigResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const listInboxSendersContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/workspaces/[id]/inbox/senders',
|
||||
params: inboxWorkspaceParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: inboxSendersResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const addInboxSenderContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/workspaces/[id]/inbox/senders',
|
||||
params: inboxWorkspaceParamsSchema,
|
||||
body: createInboxSenderBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
sender: inboxSenderSchema.extend({
|
||||
workspaceId: z.string().optional(),
|
||||
addedBy: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const removeInboxSenderContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/workspaces/[id]/inbox/senders',
|
||||
params: inboxWorkspaceParamsSchema,
|
||||
body: deleteInboxSenderBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
ok: z.literal(true),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const listInboxTasksContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/workspaces/[id]/inbox/tasks',
|
||||
params: inboxWorkspaceParamsSchema,
|
||||
query: inboxTasksQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: inboxTasksResponseSchema,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
export * from './admin'
|
||||
export * from './api-keys'
|
||||
export * from './audit-logs'
|
||||
export * from './byok-keys'
|
||||
export * from './chats'
|
||||
export * from './common'
|
||||
export * from './copilot'
|
||||
export * from './credentials'
|
||||
export * from './demo-requests'
|
||||
export * from './environment'
|
||||
export * from './execution-payloads'
|
||||
export * from './file-uploads'
|
||||
export * from './folders'
|
||||
export * from './hotspots'
|
||||
export * from './inbox'
|
||||
export * from './media'
|
||||
export * from './permission-groups'
|
||||
export * from './primitives'
|
||||
export * from './selectors'
|
||||
export * from './skills'
|
||||
export * from './storage-transfer'
|
||||
export * from './subscription'
|
||||
export * from './tool-primitives'
|
||||
export * from './tools'
|
||||
export * from './types'
|
||||
export * from './user'
|
||||
export * from './v1'
|
||||
export * from './workflows'
|
||||
export * from './workspace-file-folders'
|
||||
export * from './workspace-files'
|
||||
export * from './workspaces'
|
||||
@@ -0,0 +1,213 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import { workspacePermissionSchema } from '@/lib/api/contracts/workspaces'
|
||||
|
||||
export const invitationParamsSchema = z.object({
|
||||
id: z.string({ error: 'Invitation ID is required' }).min(1, 'Invitation ID is required'),
|
||||
})
|
||||
|
||||
export const invitationQuerySchema = z.object({
|
||||
token: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
export const invitationGrantSchema = z.object({
|
||||
workspaceId: z.string().min(1),
|
||||
permission: workspacePermissionSchema,
|
||||
})
|
||||
|
||||
export const updateInvitationBodySchema = z
|
||||
.object({
|
||||
role: z.enum(['admin', 'member']).optional(),
|
||||
grants: z.array(invitationGrantSchema).optional(),
|
||||
})
|
||||
.refine((data) => data.role !== undefined || (data.grants && data.grants.length > 0), {
|
||||
message: 'Provide a role or at least one grant update',
|
||||
})
|
||||
|
||||
export const pendingWorkspaceInvitationSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
workspaceId: z.string(),
|
||||
email: z.string(),
|
||||
token: z.string(),
|
||||
permission: workspacePermissionSchema,
|
||||
membershipIntent: z.enum(['internal', 'external']).optional(),
|
||||
status: z.string(),
|
||||
createdAt: z.string(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const batchWorkspaceInvitationBodySchema = z.object({
|
||||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
||||
invitations: z
|
||||
.array(
|
||||
z.object({
|
||||
email: z.string().trim().min(1, 'Invitation email is required'),
|
||||
permission: workspacePermissionSchema.optional(),
|
||||
})
|
||||
)
|
||||
.min(1, 'At least one invitation is required'),
|
||||
})
|
||||
|
||||
export const batchInvitationResultSchema = z
|
||||
.object({
|
||||
success: z.boolean(),
|
||||
successful: z.array(z.string()),
|
||||
added: z.array(z.string()).optional(),
|
||||
failed: z.array(z.object({ email: z.string(), error: z.string() })),
|
||||
invitations: z.array(z.record(z.string(), z.unknown())),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const removeWorkspaceMemberBodySchema = z.object({
|
||||
workspaceId: z.string().uuid(),
|
||||
})
|
||||
|
||||
export const invitationActionParamsSchema = z.object({
|
||||
id: z.string({ error: 'Invitation ID is required' }).min(1, 'Invitation ID is required'),
|
||||
})
|
||||
|
||||
export const invitationActionBodySchema = z.object({
|
||||
token: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
export const invitationDetailsSchema = z.object({
|
||||
id: z.string(),
|
||||
kind: z.enum(['organization', 'workspace']),
|
||||
email: z.string(),
|
||||
organizationId: z.string().nullable(),
|
||||
organizationName: z.string().nullable(),
|
||||
membershipIntent: z.enum(['internal', 'external']),
|
||||
role: z.string(),
|
||||
status: z.string(),
|
||||
expiresAt: z.string(),
|
||||
createdAt: z.string(),
|
||||
inviterName: z.string().nullable(),
|
||||
inviterEmail: z.string().nullable(),
|
||||
grants: z.array(
|
||||
z.object({
|
||||
workspaceId: z.string(),
|
||||
workspaceName: z.string().nullable(),
|
||||
permission: workspacePermissionSchema,
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
export const acceptInvitationResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
redirectPath: z.string(),
|
||||
invitation: z.object({
|
||||
id: z.string(),
|
||||
kind: z.enum(['organization', 'workspace']),
|
||||
organizationId: z.string().nullable(),
|
||||
acceptedWorkspaceIds: z.array(z.string()),
|
||||
}),
|
||||
})
|
||||
|
||||
const successResponseSchema = z
|
||||
.object({
|
||||
success: z.boolean(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const listWorkspaceInvitationsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/workspaces/invitations',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
invitations: z.array(pendingWorkspaceInvitationSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const batchWorkspaceInvitationsContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/workspaces/invitations/batch',
|
||||
body: batchWorkspaceInvitationBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: batchInvitationResultSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateInvitationContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/invitations/[id]',
|
||||
params: invitationParamsSchema,
|
||||
body: updateInvitationBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getInvitationContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/invitations/[id]',
|
||||
params: invitationParamsSchema,
|
||||
query: invitationQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
invitation: invitationDetailsSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const acceptInvitationContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/invitations/[id]/accept',
|
||||
params: invitationActionParamsSchema,
|
||||
body: invitationActionBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: acceptInvitationResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const rejectInvitationContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/invitations/[id]/reject',
|
||||
params: invitationActionParamsSchema,
|
||||
body: invitationActionBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const cancelInvitationContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/invitations/[id]',
|
||||
params: invitationParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const resendInvitationContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/invitations/[id]/resend',
|
||||
params: invitationParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const removeWorkspaceMemberContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/workspaces/members/[id]',
|
||||
params: invitationParamsSchema,
|
||||
body: removeWorkspaceMemberBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export type PendingInvitationRow = z.infer<typeof pendingWorkspaceInvitationSchema>
|
||||
export type BatchInvitationResult = z.infer<typeof batchInvitationResultSchema>
|
||||
export type InvitationDetails = z.infer<typeof invitationDetailsSchema>
|
||||
@@ -0,0 +1,174 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
knowledgeBaseParamsSchema,
|
||||
nullableWireDateSchema,
|
||||
successResponseSchema,
|
||||
wireDateSchema,
|
||||
} from '@/lib/api/contracts/knowledge/shared'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import type { StrategyOptions } from '@/lib/chunkers/types'
|
||||
import { KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH } from '@/lib/knowledge/constants'
|
||||
|
||||
export const knowledgeScopeSchema = z.enum(['active', 'archived', 'all'])
|
||||
export type KnowledgeScope = z.output<typeof knowledgeScopeSchema>
|
||||
|
||||
export const listKnowledgeBasesQuerySchema = z.object({
|
||||
workspaceId: z.string().min(1).optional(),
|
||||
scope: knowledgeScopeSchema.default('active'),
|
||||
})
|
||||
|
||||
export const chunkingStrategyOptionsSchema = z
|
||||
.object({
|
||||
pattern: z.string().max(500).optional(),
|
||||
separators: z.array(z.string()).optional(),
|
||||
recipe: z.enum(['plain', 'markdown', 'code']).optional(),
|
||||
strictBoundaries: z.boolean().optional(),
|
||||
})
|
||||
.strict() satisfies z.ZodType<StrategyOptions>
|
||||
|
||||
export const chunkingConfigSchema = z
|
||||
.object({
|
||||
maxSize: z.number().min(100).max(4000),
|
||||
minSize: z.number().min(1).max(2000),
|
||||
overlap: z.number().min(0).max(500),
|
||||
strategy: z.enum(['auto', 'text', 'regex', 'recursive', 'sentence', 'token']).optional(),
|
||||
strategyOptions: chunkingStrategyOptionsSchema.optional(),
|
||||
})
|
||||
.refine((data) => data.minSize < data.maxSize * 4, {
|
||||
message: 'Min chunk size (characters) must be less than max chunk size (tokens × 4)',
|
||||
})
|
||||
.refine((data) => data.overlap < data.maxSize, {
|
||||
message: 'Overlap must be less than max chunk size',
|
||||
})
|
||||
.refine(
|
||||
(data) => data.strategy !== 'regex' || typeof data.strategyOptions?.pattern === 'string',
|
||||
{
|
||||
message: 'Regex pattern is required when using the regex chunking strategy',
|
||||
}
|
||||
)
|
||||
.refine((data) => data.strategy === 'regex' || data.strategyOptions?.strictBoundaries !== true, {
|
||||
message: 'strictBoundaries is only valid for the regex chunking strategy',
|
||||
})
|
||||
|
||||
export const createKnowledgeBaseBodySchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
description: z
|
||||
.string()
|
||||
.max(
|
||||
KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH,
|
||||
`Description must be ${KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH} characters or less`
|
||||
)
|
||||
.optional(),
|
||||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
||||
embeddingModel: z.literal('text-embedding-3-small').default('text-embedding-3-small'),
|
||||
embeddingDimension: z.literal(1536).default(1536),
|
||||
chunkingConfig: chunkingConfigSchema.default({
|
||||
maxSize: 1024,
|
||||
minSize: 100,
|
||||
overlap: 200,
|
||||
}),
|
||||
})
|
||||
|
||||
export const updateKnowledgeBaseBodySchema = createKnowledgeBaseBodySchema
|
||||
.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
})
|
||||
.partial()
|
||||
.extend({
|
||||
chunkingConfig: chunkingConfigSchema.optional(),
|
||||
workspaceId: z.string().nullable().optional(),
|
||||
embeddingModel: z.literal('text-embedding-3-small').optional(),
|
||||
embeddingDimension: z.literal(1536).optional(),
|
||||
})
|
||||
|
||||
const knowledgeChunkingConfigSchema = z
|
||||
.object({
|
||||
maxSize: z.number(),
|
||||
minSize: z.number(),
|
||||
overlap: z.number(),
|
||||
strategy: z.enum(['auto', 'text', 'regex', 'recursive', 'sentence', 'token']).optional(),
|
||||
strategyOptions: chunkingStrategyOptionsSchema.optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const knowledgeBaseDataSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
userId: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
tokenCount: z.number(),
|
||||
embeddingModel: z.string(),
|
||||
embeddingDimension: z.number(),
|
||||
chunkingConfig: knowledgeChunkingConfigSchema,
|
||||
createdAt: wireDateSchema,
|
||||
updatedAt: wireDateSchema,
|
||||
deletedAt: nullableWireDateSchema,
|
||||
workspaceId: z.string().nullable(),
|
||||
docCount: z.number().optional(),
|
||||
connectorTypes: z.array(z.string()).optional(),
|
||||
})
|
||||
.passthrough()
|
||||
export type KnowledgeBaseData = z.output<typeof knowledgeBaseDataSchema>
|
||||
|
||||
export const listKnowledgeBasesContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/knowledge',
|
||||
query: listKnowledgeBasesQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(z.array(knowledgeBaseDataSchema)),
|
||||
},
|
||||
})
|
||||
|
||||
export const createKnowledgeBaseContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/knowledge',
|
||||
body: createKnowledgeBaseBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(knowledgeBaseDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const getKnowledgeBaseContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/knowledge/[id]',
|
||||
params: knowledgeBaseParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(knowledgeBaseDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateKnowledgeBaseContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/knowledge/[id]',
|
||||
params: knowledgeBaseParamsSchema,
|
||||
body: updateKnowledgeBaseBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(knowledgeBaseDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteKnowledgeBaseContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/knowledge/[id]',
|
||||
params: knowledgeBaseParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(z.object({ message: z.string() })),
|
||||
},
|
||||
})
|
||||
|
||||
export const restoreKnowledgeBaseContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/knowledge/[id]/restore',
|
||||
params: knowledgeBaseParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({ success: z.literal(true) }).passthrough(),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,152 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
documentBooleanFieldSchema,
|
||||
documentDateFieldSchema,
|
||||
documentNumberFieldSchema,
|
||||
documentTagFieldSchema,
|
||||
knowledgeChunkParamsSchema,
|
||||
knowledgeDocumentParamsSchema,
|
||||
paginationSchema,
|
||||
successResponseSchema,
|
||||
wireDateSchema,
|
||||
} from '@/lib/api/contracts/knowledge/shared'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const listKnowledgeChunksQuerySchema = z.object({
|
||||
search: z.string().optional(),
|
||||
enabled: z.enum(['true', 'false', 'all']).optional().default('all'),
|
||||
limit: z.coerce.number().min(1).max(100).optional().default(50),
|
||||
offset: z.coerce.number().min(0).optional().default(0),
|
||||
sortBy: z.enum(['chunkIndex', 'tokenCount', 'enabled']).optional().default('chunkIndex'),
|
||||
sortOrder: z.enum(['asc', 'desc']).optional().default('asc'),
|
||||
})
|
||||
|
||||
export const createChunkBodySchema = z.object({
|
||||
content: z.string().min(1, 'Content is required').max(10000, 'Content too long'),
|
||||
enabled: z.boolean().optional().default(true),
|
||||
})
|
||||
|
||||
export const updateChunkBodySchema = createChunkBodySchema.partial()
|
||||
|
||||
export const bulkChunkOperationBodySchema = z.object({
|
||||
operation: z.enum(['enable', 'disable', 'delete']),
|
||||
chunkIds: z.array(z.string()).min(1, 'At least one chunk ID is required').max(100),
|
||||
})
|
||||
|
||||
export const bulkChunkOperationDataSchema = z.object({
|
||||
operation: z.string(),
|
||||
successCount: z.number(),
|
||||
errorCount: z.number(),
|
||||
processed: z.number(),
|
||||
errors: z.array(z.string()),
|
||||
})
|
||||
export type BulkChunkOperationData = z.output<typeof bulkChunkOperationDataSchema>
|
||||
|
||||
export const chunkDataSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
chunkIndex: z.number(),
|
||||
content: z.string(),
|
||||
contentLength: z.number(),
|
||||
tokenCount: z.number(),
|
||||
enabled: z.boolean(),
|
||||
startOffset: z.number(),
|
||||
endOffset: z.number(),
|
||||
tag1: documentTagFieldSchema,
|
||||
tag2: documentTagFieldSchema,
|
||||
tag3: documentTagFieldSchema,
|
||||
tag4: documentTagFieldSchema,
|
||||
tag5: documentTagFieldSchema,
|
||||
tag6: documentTagFieldSchema,
|
||||
tag7: documentTagFieldSchema,
|
||||
number1: documentNumberFieldSchema,
|
||||
number2: documentNumberFieldSchema,
|
||||
number3: documentNumberFieldSchema,
|
||||
number4: documentNumberFieldSchema,
|
||||
number5: documentNumberFieldSchema,
|
||||
date1: documentDateFieldSchema,
|
||||
date2: documentDateFieldSchema,
|
||||
boolean1: documentBooleanFieldSchema,
|
||||
boolean2: documentBooleanFieldSchema,
|
||||
boolean3: documentBooleanFieldSchema,
|
||||
createdAt: wireDateSchema,
|
||||
updatedAt: wireDateSchema,
|
||||
})
|
||||
.passthrough()
|
||||
export type ChunkData = z.output<typeof chunkDataSchema>
|
||||
|
||||
export const chunksPaginationSchema = paginationSchema
|
||||
export type ChunksPagination = z.output<typeof chunksPaginationSchema>
|
||||
|
||||
export type KnowledgeChunksResponse = {
|
||||
chunks: ChunkData[]
|
||||
pagination: ChunksPagination
|
||||
}
|
||||
|
||||
export const listKnowledgeChunksContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/knowledge/[id]/documents/[documentId]/chunks',
|
||||
params: knowledgeDocumentParamsSchema,
|
||||
query: listKnowledgeChunksQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
data: z.array(chunkDataSchema),
|
||||
pagination: chunksPaginationSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const createKnowledgeChunkContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/knowledge/[id]/documents/[documentId]/chunks',
|
||||
params: knowledgeDocumentParamsSchema,
|
||||
body: createChunkBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(chunkDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const getKnowledgeChunkContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/knowledge/[id]/documents/[documentId]/chunks/[chunkId]',
|
||||
params: knowledgeChunkParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(chunkDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateKnowledgeChunkContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/knowledge/[id]/documents/[documentId]/chunks/[chunkId]',
|
||||
params: knowledgeChunkParamsSchema,
|
||||
body: updateChunkBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(chunkDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteKnowledgeChunkContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/knowledge/[id]/documents/[documentId]/chunks/[chunkId]',
|
||||
params: knowledgeChunkParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(z.object({ message: z.string() })),
|
||||
},
|
||||
})
|
||||
|
||||
export const bulkKnowledgeChunksContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/knowledge/[id]/documents/[documentId]/chunks',
|
||||
params: knowledgeDocumentParamsSchema,
|
||||
body: bulkChunkOperationBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(bulkChunkOperationDataSchema),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,194 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
knowledgeBaseParamsSchema,
|
||||
knowledgeConnectorParamsSchema,
|
||||
successResponseSchema,
|
||||
} from '@/lib/api/contracts/knowledge/shared'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const createConnectorBodySchema = z.object({
|
||||
connectorType: z.string().min(1),
|
||||
credentialId: z.string().min(1).optional(),
|
||||
apiKey: z.string().min(1).optional(),
|
||||
sourceConfig: z.record(z.string(), z.unknown()),
|
||||
syncIntervalMinutes: z.number().int().min(0).default(1440),
|
||||
})
|
||||
|
||||
export const updateConnectorBodySchema = z.object({
|
||||
sourceConfig: z.record(z.string(), z.unknown()).optional(),
|
||||
syncIntervalMinutes: z.number().int().min(0).optional(),
|
||||
status: z.enum(['active', 'paused']).optional(),
|
||||
})
|
||||
|
||||
export const deleteConnectorQuerySchema = z.object({
|
||||
deleteDocuments: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const connectorDocumentsQuerySchema = z.object({
|
||||
includeExcluded: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const connectorDocumentsPatchBodySchema = z.object({
|
||||
operation: z.enum(['restore', 'exclude']),
|
||||
documentIds: z.array(z.string()).min(1),
|
||||
})
|
||||
|
||||
export const connectorDataSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
knowledgeBaseId: z.string(),
|
||||
connectorType: z.string(),
|
||||
credentialId: z.string().nullable(),
|
||||
sourceConfig: z.record(z.string(), z.unknown()),
|
||||
syncMode: z.string().nullable(),
|
||||
syncIntervalMinutes: z.number(),
|
||||
status: z.enum(['active', 'paused', 'syncing', 'error', 'disabled']),
|
||||
lastSyncAt: z.string().nullable(),
|
||||
lastSyncError: z.string().nullable(),
|
||||
lastSyncDocCount: z.number().nullable(),
|
||||
nextSyncAt: z.string().nullable(),
|
||||
consecutiveFailures: z.number(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
.passthrough()
|
||||
export type ConnectorData = z.output<typeof connectorDataSchema>
|
||||
|
||||
export const syncLogDataSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
connectorId: z.string(),
|
||||
status: z.string(),
|
||||
startedAt: z.string(),
|
||||
completedAt: z.string().nullable(),
|
||||
docsAdded: z.number(),
|
||||
docsUpdated: z.number(),
|
||||
docsDeleted: z.number(),
|
||||
docsUnchanged: z.number(),
|
||||
docsFailed: z.number(),
|
||||
errorMessage: z.string().nullable(),
|
||||
})
|
||||
.passthrough()
|
||||
export type SyncLogData = z.output<typeof syncLogDataSchema>
|
||||
|
||||
export const connectorDetailDataSchema = connectorDataSchema.extend({
|
||||
syncLogs: z.array(syncLogDataSchema),
|
||||
})
|
||||
export type ConnectorDetailData = z.output<typeof connectorDetailDataSchema>
|
||||
|
||||
export const connectorDocumentDataSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
filename: z.string(),
|
||||
externalId: z.string().nullable(),
|
||||
sourceUrl: z.string().nullable(),
|
||||
enabled: z.boolean(),
|
||||
deletedAt: z.string().nullable().default(null),
|
||||
userExcluded: z.boolean(),
|
||||
uploadedAt: z.string(),
|
||||
processingStatus: z.string(),
|
||||
})
|
||||
.passthrough()
|
||||
export type ConnectorDocumentData = z.output<typeof connectorDocumentDataSchema>
|
||||
|
||||
export const connectorDocumentsDataSchema = z.object({
|
||||
documents: z.array(connectorDocumentDataSchema),
|
||||
counts: z.object({ active: z.number(), excluded: z.number() }),
|
||||
})
|
||||
export type ConnectorDocumentsData = z.output<typeof connectorDocumentsDataSchema>
|
||||
|
||||
export const listKnowledgeConnectorsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/knowledge/[id]/connectors',
|
||||
params: knowledgeBaseParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(z.array(connectorDataSchema)),
|
||||
},
|
||||
})
|
||||
|
||||
export const createKnowledgeConnectorContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/knowledge/[id]/connectors',
|
||||
params: knowledgeBaseParamsSchema,
|
||||
body: createConnectorBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(connectorDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const getKnowledgeConnectorContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/knowledge/[id]/connectors/[connectorId]',
|
||||
params: knowledgeConnectorParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(connectorDetailDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateKnowledgeConnectorContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/knowledge/[id]/connectors/[connectorId]',
|
||||
params: knowledgeConnectorParamsSchema,
|
||||
body: updateConnectorBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(connectorDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteKnowledgeConnectorContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/knowledge/[id]/connectors/[connectorId]',
|
||||
params: knowledgeConnectorParamsSchema,
|
||||
query: deleteConnectorQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({ success: z.literal(true) }),
|
||||
},
|
||||
})
|
||||
|
||||
export const triggerKnowledgeConnectorSyncContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/knowledge/[id]/connectors/[connectorId]/sync',
|
||||
params: knowledgeConnectorParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
message: z.string(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const listKnowledgeConnectorDocumentsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/knowledge/[id]/connectors/[connectorId]/documents',
|
||||
params: knowledgeConnectorParamsSchema,
|
||||
query: connectorDocumentsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(connectorDocumentsDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const patchKnowledgeConnectorDocumentsContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/knowledge/[id]/connectors/[connectorId]/documents',
|
||||
params: knowledgeConnectorParamsSchema,
|
||||
body: connectorDocumentsPatchBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(
|
||||
z
|
||||
.object({
|
||||
excludedCount: z.number().optional(),
|
||||
restoredCount: z.number().optional(),
|
||||
documentIds: z.array(z.string()).optional(),
|
||||
})
|
||||
.passthrough()
|
||||
),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
listKnowledgeDocumentsQuerySchema,
|
||||
parseDocumentTagFiltersParam,
|
||||
} from '@/lib/api/contracts/knowledge/documents'
|
||||
|
||||
describe('listKnowledgeDocumentsQuerySchema.tagFilters', () => {
|
||||
it('keeps tagFilters a raw string (must NOT transform to an array)', () => {
|
||||
// A transform-to-array here breaks requestJson outbound serialization
|
||||
// (the array serializes as "[object Object]"). The wire type must stay a
|
||||
// string; decoding happens server-side via parseDocumentTagFiltersParam.
|
||||
const tagFilters = JSON.stringify([
|
||||
{ tagSlot: 'tag1', fieldType: 'text', operator: 'contains', value: 'x' },
|
||||
])
|
||||
const parsed = listKnowledgeDocumentsQuerySchema.parse({ tagFilters })
|
||||
expect(parsed.tagFilters).toBe(tagFilters)
|
||||
expect(typeof parsed.tagFilters).toBe('string')
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseDocumentTagFiltersParam', () => {
|
||||
it('returns undefined for an absent param', () => {
|
||||
expect(parseDocumentTagFiltersParam(undefined)).toBeUndefined()
|
||||
expect(parseDocumentTagFiltersParam('')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('decodes a valid JSON array of filters', () => {
|
||||
const filters = [
|
||||
{ tagSlot: 'tag1', fieldType: 'text', operator: 'contains', value: 'x' },
|
||||
{ tagSlot: 'date1', fieldType: 'date', operator: 'eq', value: '2026-04-21' },
|
||||
]
|
||||
expect(parseDocumentTagFiltersParam(JSON.stringify(filters))).toEqual(filters)
|
||||
})
|
||||
|
||||
it('throws on malformed JSON', () => {
|
||||
expect(() => parseDocumentTagFiltersParam('[object Object]')).toThrow()
|
||||
expect(() => parseDocumentTagFiltersParam('{not json')).toThrow()
|
||||
})
|
||||
|
||||
it('throws when the shape is wrong', () => {
|
||||
expect(() => parseDocumentTagFiltersParam(JSON.stringify([{ tagSlot: '' }]))).toThrow()
|
||||
})
|
||||
|
||||
it('rejects an operator that is not valid for the field type', () => {
|
||||
// unknown operator
|
||||
expect(() =>
|
||||
parseDocumentTagFiltersParam(
|
||||
JSON.stringify([{ tagSlot: 'tag1', fieldType: 'text', operator: 'bogus', value: 'x' }])
|
||||
)
|
||||
).toThrow()
|
||||
// valid operator name, wrong field type (contains is text-only)
|
||||
expect(() =>
|
||||
parseDocumentTagFiltersParam(
|
||||
JSON.stringify([
|
||||
{ tagSlot: 'number1', fieldType: 'number', operator: 'contains', value: '1' },
|
||||
])
|
||||
)
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('rejects a fieldType that does not match the tag slot', () => {
|
||||
// number1 is a numeric column; claiming it is text must fail
|
||||
expect(() =>
|
||||
parseDocumentTagFiltersParam(
|
||||
JSON.stringify([
|
||||
{ tagSlot: 'number1', fieldType: 'text', operator: 'contains', value: 'x' },
|
||||
])
|
||||
)
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('rejects an unknown tag slot', () => {
|
||||
expect(() =>
|
||||
parseDocumentTagFiltersParam(
|
||||
JSON.stringify([{ tagSlot: 'tag99', fieldType: 'text', operator: 'eq', value: 'x' }])
|
||||
)
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('rejects values that are unusable for the field type', () => {
|
||||
// non-numeric value on a number field
|
||||
expect(() =>
|
||||
parseDocumentTagFiltersParam(
|
||||
JSON.stringify([{ tagSlot: 'number1', fieldType: 'number', operator: 'eq', value: 'abc' }])
|
||||
)
|
||||
).toThrow()
|
||||
// non-date value on a date field
|
||||
expect(() =>
|
||||
parseDocumentTagFiltersParam(
|
||||
JSON.stringify([{ tagSlot: 'date1', fieldType: 'date', operator: 'eq', value: 'nope' }])
|
||||
)
|
||||
).toThrow()
|
||||
// well-formed but impossible calendar dates (would 500 on ::date)
|
||||
for (const value of ['2026-02-30', '2026-99-99', '2026-13-01', '2026-00-10']) {
|
||||
expect(() =>
|
||||
parseDocumentTagFiltersParam(
|
||||
JSON.stringify([{ tagSlot: 'date1', fieldType: 'date', operator: 'eq', value }])
|
||||
)
|
||||
).toThrow()
|
||||
}
|
||||
// non-boolean value on a boolean field
|
||||
expect(() =>
|
||||
parseDocumentTagFiltersParam(
|
||||
JSON.stringify([
|
||||
{ tagSlot: 'boolean1', fieldType: 'boolean', operator: 'eq', value: 'maybe' },
|
||||
])
|
||||
)
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('rejects a between filter missing a usable upper bound', () => {
|
||||
expect(() =>
|
||||
parseDocumentTagFiltersParam(
|
||||
JSON.stringify([
|
||||
{
|
||||
tagSlot: 'number1',
|
||||
fieldType: 'number',
|
||||
operator: 'between',
|
||||
value: '1',
|
||||
valueTo: 'x',
|
||||
},
|
||||
])
|
||||
)
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('accepts a valid number, date, boolean, and between filter', () => {
|
||||
const filters = [
|
||||
{ tagSlot: 'number1', fieldType: 'number', operator: 'gte', value: '42' },
|
||||
{ tagSlot: 'date1', fieldType: 'date', operator: 'eq', value: '2026-04-21' },
|
||||
{ tagSlot: 'boolean1', fieldType: 'boolean', operator: 'eq', value: 'true' },
|
||||
{
|
||||
tagSlot: 'number2',
|
||||
fieldType: 'number',
|
||||
operator: 'between',
|
||||
value: '1',
|
||||
valueTo: '10',
|
||||
},
|
||||
]
|
||||
expect(parseDocumentTagFiltersParam(JSON.stringify(filters))).toEqual(filters)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,381 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
documentBooleanFieldSchema,
|
||||
documentDateFieldSchema,
|
||||
documentNumberFieldSchema,
|
||||
documentTagFieldSchema,
|
||||
knowledgeBaseParamsSchema,
|
||||
knowledgeDocumentFileUrlSchema,
|
||||
knowledgeDocumentParamsSchema,
|
||||
nullableWireDateSchema,
|
||||
paginationSchema,
|
||||
successResponseSchema,
|
||||
wireDateSchema,
|
||||
} from '@/lib/api/contracts/knowledge/shared'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import { getFieldTypeForSlot } from '@/lib/knowledge/constants'
|
||||
import { getOperatorsForFieldType, isValidFilterValue } from '@/lib/knowledge/filters/types'
|
||||
|
||||
export const documentTagFilterSchema = z
|
||||
.object({
|
||||
tagSlot: z.string().min(1),
|
||||
fieldType: z.enum(['text', 'number', 'date', 'boolean']),
|
||||
operator: z.string().min(1),
|
||||
value: z.unknown(),
|
||||
valueTo: z.unknown().optional(),
|
||||
})
|
||||
.superRefine((filter, ctx) => {
|
||||
// The tag slot determines the column type, so validate against the slot
|
||||
// (the source of truth) — not just the client-supplied fieldType. Rejecting
|
||||
// unknown slots, type mismatches, and bad operators at the boundary returns
|
||||
// a 400 instead of the query builder silently dropping or mis-handling the
|
||||
// filter (e.g. a text `contains` aimed at a numeric column).
|
||||
const slotFieldType = getFieldTypeForSlot(filter.tagSlot)
|
||||
if (slotFieldType === null) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['tagSlot'],
|
||||
message: `Unknown tag slot "${filter.tagSlot}"`,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (slotFieldType !== filter.fieldType) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['fieldType'],
|
||||
message: `fieldType "${filter.fieldType}" does not match tag slot "${filter.tagSlot}" (expected "${slotFieldType}")`,
|
||||
})
|
||||
return
|
||||
}
|
||||
const validOperators = getOperatorsForFieldType(filter.fieldType).map((op) => op.value)
|
||||
if (!validOperators.includes(filter.operator)) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['operator'],
|
||||
message: `Unsupported operator "${filter.operator}" for a ${filter.fieldType} tag filter`,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!isValidFilterValue(filter.fieldType, filter.value)) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['value'],
|
||||
message: `Invalid value for a ${filter.fieldType} tag filter`,
|
||||
})
|
||||
}
|
||||
// `between` is only valid for number/date (enforced by the operator check
|
||||
// above), and needs a usable upper bound.
|
||||
if (filter.operator === 'between' && !isValidFilterValue(filter.fieldType, filter.valueTo)) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['valueTo'],
|
||||
message: `Invalid second value for a ${filter.fieldType} "between" tag filter`,
|
||||
})
|
||||
}
|
||||
})
|
||||
export type DocumentTagFilter = z.output<typeof documentTagFilterSchema>
|
||||
|
||||
export const listKnowledgeDocumentsQuerySchema = z.object({
|
||||
enabledFilter: z.enum(['all', 'enabled', 'disabled']).optional(),
|
||||
search: z.string().optional(),
|
||||
limit: z.coerce.number().int().min(1).max(100).optional().default(50),
|
||||
offset: z.coerce.number().int().min(0).optional().default(0),
|
||||
sortBy: z
|
||||
.enum([
|
||||
'filename',
|
||||
'fileSize',
|
||||
'tokenCount',
|
||||
'chunkCount',
|
||||
'uploadedAt',
|
||||
'processingStatus',
|
||||
'enabled',
|
||||
])
|
||||
.optional(),
|
||||
sortOrder: z.enum(['asc', 'desc']).optional(),
|
||||
// A query param is a string on the wire, so `tagFilters` is carried as a JSON
|
||||
// string and decoded by the route via `parseDocumentTagFiltersParam`. It must
|
||||
// NOT be a `.transform()` to an array here: the client's `requestJson` parses
|
||||
// the query before serializing it, so a transform would turn the string into
|
||||
// an array that serializes to `tagFilters=[object Object]` and 400s the route.
|
||||
tagFilters: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Decodes the `tagFilters` query string (a JSON array) into validated filters.
|
||||
* Throws on malformed JSON or a shape mismatch; callers map that to a 400.
|
||||
*/
|
||||
export function parseDocumentTagFiltersParam(
|
||||
value: string | undefined
|
||||
): DocumentTagFilter[] | undefined {
|
||||
if (!value) return undefined
|
||||
return z.array(documentTagFilterSchema).parse(JSON.parse(value))
|
||||
}
|
||||
|
||||
export const createDocumentBodySchema = z.object({
|
||||
filename: z.string().min(1, 'Filename is required'),
|
||||
fileUrl: knowledgeDocumentFileUrlSchema,
|
||||
fileSize: z.number().min(1, 'File size must be greater than 0'),
|
||||
mimeType: z.string().min(1, 'MIME type is required'),
|
||||
tag1: z.string().optional(),
|
||||
tag2: z.string().optional(),
|
||||
tag3: z.string().optional(),
|
||||
tag4: z.string().optional(),
|
||||
tag5: z.string().optional(),
|
||||
tag6: z.string().optional(),
|
||||
tag7: z.string().optional(),
|
||||
documentTagsData: z.string().optional(),
|
||||
})
|
||||
|
||||
export const bulkCreateDocumentsBodySchema = z.object({
|
||||
documents: z.array(createDocumentBodySchema),
|
||||
processingOptions: z
|
||||
.object({
|
||||
recipe: z.string().optional(),
|
||||
lang: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
bulk: z.literal(true),
|
||||
workflowId: z.string().optional(),
|
||||
})
|
||||
|
||||
const singleCreateDocumentBodySchema = createDocumentBodySchema.extend({
|
||||
bulk: z.literal(false),
|
||||
workflowId: z.string().optional(),
|
||||
})
|
||||
|
||||
const createKnowledgeDocumentsBodyDiscriminatedUnion = z.discriminatedUnion('bulk', [
|
||||
bulkCreateDocumentsBodySchema,
|
||||
singleCreateDocumentBodySchema,
|
||||
])
|
||||
|
||||
export const createKnowledgeDocumentsBodySchema = z
|
||||
.object({ bulk: z.boolean().default(false) })
|
||||
.passthrough()
|
||||
.pipe(createKnowledgeDocumentsBodyDiscriminatedUnion)
|
||||
export type CreateKnowledgeDocumentsBody = z.input<typeof createKnowledgeDocumentsBodySchema>
|
||||
export type BulkCreateDocumentsBody = z.input<typeof bulkCreateDocumentsBodySchema>
|
||||
export type SingleCreateDocumentBody = z.input<typeof singleCreateDocumentBodySchema>
|
||||
|
||||
export const upsertDocumentBodySchema = z.object({
|
||||
documentId: z.string().optional(),
|
||||
filename: z.string().min(1, 'Filename is required'),
|
||||
fileUrl: knowledgeDocumentFileUrlSchema,
|
||||
fileSize: z.number().min(1, 'File size must be greater than 0'),
|
||||
mimeType: z.string().min(1, 'MIME type is required'),
|
||||
documentTagsData: z.string().optional(),
|
||||
processingOptions: z
|
||||
.object({
|
||||
recipe: z.string().optional(),
|
||||
lang: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
workflowId: z.string().optional(),
|
||||
})
|
||||
export type UpsertDocumentBody = z.output<typeof upsertDocumentBodySchema>
|
||||
|
||||
export const bulkCreateDocumentsResponseSchema = z.object({
|
||||
total: z.number(),
|
||||
documentsCreated: z.array(
|
||||
z.object({
|
||||
documentId: z.string(),
|
||||
filename: z.string(),
|
||||
status: z.string(),
|
||||
})
|
||||
),
|
||||
processingMethod: z.string(),
|
||||
processingConfig: z
|
||||
.object({
|
||||
maxConcurrentDocuments: z.number(),
|
||||
batchSize: z.number(),
|
||||
totalBatches: z.number(),
|
||||
})
|
||||
.passthrough(),
|
||||
})
|
||||
|
||||
export const updateDocumentBodySchema = z.object({
|
||||
filename: z.string().min(1, 'Filename is required').optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
chunkCount: z.number().min(0).optional(),
|
||||
tokenCount: z.number().min(0).optional(),
|
||||
characterCount: z.number().min(0).optional(),
|
||||
processingStatus: z.enum(['pending', 'processing', 'completed', 'failed']).optional(),
|
||||
processingError: z.string().optional(),
|
||||
markFailedDueToTimeout: z.boolean().optional(),
|
||||
retryProcessing: z.boolean().optional(),
|
||||
tag1: z.string().optional(),
|
||||
tag2: z.string().optional(),
|
||||
tag3: z.string().optional(),
|
||||
tag4: z.string().optional(),
|
||||
tag5: z.string().optional(),
|
||||
tag6: z.string().optional(),
|
||||
tag7: z.string().optional(),
|
||||
number1: z.string().optional(),
|
||||
number2: z.string().optional(),
|
||||
number3: z.string().optional(),
|
||||
number4: z.string().optional(),
|
||||
number5: z.string().optional(),
|
||||
date1: z.string().optional(),
|
||||
date2: z.string().optional(),
|
||||
boolean1: z.string().optional(),
|
||||
boolean2: z.string().optional(),
|
||||
boolean3: z.string().optional(),
|
||||
})
|
||||
|
||||
export const updateDocumentTagsBodySchema = z.record(z.string(), z.string())
|
||||
|
||||
export const bulkDocumentOperationBodySchema = z
|
||||
.object({
|
||||
operation: z.enum(['enable', 'disable', 'delete']),
|
||||
documentIds: z.array(z.string()).min(1).max(100).optional(),
|
||||
selectAll: z.boolean().optional(),
|
||||
enabledFilter: z.enum(['all', 'enabled', 'disabled']).optional(),
|
||||
})
|
||||
.refine((data) => data.selectAll || (data.documentIds && data.documentIds.length > 0), {
|
||||
message: 'Either selectAll must be true or documentIds must be provided',
|
||||
})
|
||||
|
||||
export const bulkDocumentOperationDataSchema = z.object({
|
||||
operation: z.string().optional(),
|
||||
successCount: z.number(),
|
||||
failedCount: z.number().optional(),
|
||||
updatedDocuments: z
|
||||
.array(z.object({ id: z.string(), enabled: z.boolean().optional() }))
|
||||
.optional(),
|
||||
})
|
||||
export type BulkDocumentOperationData = z.output<typeof bulkDocumentOperationDataSchema>
|
||||
|
||||
export const documentDataSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
knowledgeBaseId: z.string(),
|
||||
filename: z.string(),
|
||||
fileUrl: z.string(),
|
||||
fileSize: z.number(),
|
||||
mimeType: z.string(),
|
||||
chunkCount: z.number(),
|
||||
tokenCount: z.number(),
|
||||
characterCount: z.number(),
|
||||
processingStatus: z.enum(['pending', 'processing', 'completed', 'failed']),
|
||||
processingStartedAt: nullableWireDateSchema.optional(),
|
||||
processingCompletedAt: nullableWireDateSchema.optional(),
|
||||
processingError: z.string().nullable().optional(),
|
||||
enabled: z.boolean(),
|
||||
uploadedAt: wireDateSchema,
|
||||
tag1: documentTagFieldSchema,
|
||||
tag2: documentTagFieldSchema,
|
||||
tag3: documentTagFieldSchema,
|
||||
tag4: documentTagFieldSchema,
|
||||
tag5: documentTagFieldSchema,
|
||||
tag6: documentTagFieldSchema,
|
||||
tag7: documentTagFieldSchema,
|
||||
number1: documentNumberFieldSchema,
|
||||
number2: documentNumberFieldSchema,
|
||||
number3: documentNumberFieldSchema,
|
||||
number4: documentNumberFieldSchema,
|
||||
number5: documentNumberFieldSchema,
|
||||
date1: documentDateFieldSchema,
|
||||
date2: documentDateFieldSchema,
|
||||
boolean1: documentBooleanFieldSchema,
|
||||
boolean2: documentBooleanFieldSchema,
|
||||
boolean3: documentBooleanFieldSchema,
|
||||
connectorId: z.string().nullable().optional(),
|
||||
connectorType: z.string().nullable().optional(),
|
||||
sourceUrl: z.string().nullable().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
export type DocumentData = z.output<typeof documentDataSchema>
|
||||
|
||||
export const documentsPaginationSchema = paginationSchema
|
||||
export type DocumentsPagination = z.output<typeof documentsPaginationSchema>
|
||||
|
||||
export const knowledgeDocumentsDataSchema = z.object({
|
||||
documents: z.array(documentDataSchema),
|
||||
pagination: documentsPaginationSchema,
|
||||
})
|
||||
export type KnowledgeDocumentsResponse = z.output<typeof knowledgeDocumentsDataSchema>
|
||||
|
||||
export const getKnowledgeDocumentContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/knowledge/[id]/documents/[documentId]',
|
||||
params: knowledgeDocumentParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(documentDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const listKnowledgeDocumentsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/knowledge/[id]/documents',
|
||||
params: knowledgeBaseParamsSchema,
|
||||
query: listKnowledgeDocumentsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(knowledgeDocumentsDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const createKnowledgeDocumentsContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/knowledge/[id]/documents',
|
||||
params: knowledgeBaseParamsSchema,
|
||||
body: createKnowledgeDocumentsBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(z.union([bulkCreateDocumentsResponseSchema, documentDataSchema])),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateKnowledgeDocumentContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/knowledge/[id]/documents/[documentId]',
|
||||
params: knowledgeDocumentParamsSchema,
|
||||
body: updateDocumentBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(documentDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateKnowledgeDocumentTagsContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/knowledge/[id]/documents/[documentId]',
|
||||
params: knowledgeDocumentParamsSchema,
|
||||
body: updateDocumentTagsBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(documentDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteKnowledgeDocumentContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/knowledge/[id]/documents/[documentId]',
|
||||
params: knowledgeDocumentParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(z.unknown()),
|
||||
},
|
||||
})
|
||||
|
||||
export const bulkKnowledgeDocumentsContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/knowledge/[id]/documents',
|
||||
params: knowledgeBaseParamsSchema,
|
||||
body: bulkDocumentOperationBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(bulkDocumentOperationDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const upsertKnowledgeDocumentContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/knowledge/[id]/documents/upsert',
|
||||
params: knowledgeBaseParamsSchema,
|
||||
body: upsertDocumentBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(documentDataSchema),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from '@/lib/api/contracts/knowledge/base'
|
||||
export * from '@/lib/api/contracts/knowledge/chunks'
|
||||
export * from '@/lib/api/contracts/knowledge/connectors'
|
||||
export * from '@/lib/api/contracts/knowledge/documents'
|
||||
export * from '@/lib/api/contracts/knowledge/search'
|
||||
export * from '@/lib/api/contracts/knowledge/tags'
|
||||
@@ -0,0 +1,68 @@
|
||||
import { z } from 'zod'
|
||||
import { DEFAULT_RERANKER_MODEL, rerankerModelSchema } from '@/lib/knowledge/reranker-models'
|
||||
|
||||
export const knowledgeSearchTagFilterSchema = z.object({
|
||||
tagName: z.string(),
|
||||
tagSlot: z.string().optional(),
|
||||
fieldType: z.enum(['text', 'number', 'date', 'boolean']).optional(),
|
||||
operator: z.string().default('eq'),
|
||||
value: z.union([z.string(), z.number(), z.boolean()]),
|
||||
valueTo: z.union([z.string(), z.number()]).optional(),
|
||||
})
|
||||
|
||||
export const knowledgeSearchBodySchema = z
|
||||
.object({
|
||||
knowledgeBaseIds: z.union([
|
||||
z.string().min(1, 'Knowledge base ID is required'),
|
||||
z.array(z.string().min(1)).min(1, 'At least one knowledge base ID is required'),
|
||||
]),
|
||||
query: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => val || undefined),
|
||||
topK: z
|
||||
.number()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.optional()
|
||||
.nullable()
|
||||
.default(10)
|
||||
.transform((val) => val ?? 10),
|
||||
tagFilters: z
|
||||
.array(knowledgeSearchTagFilterSchema)
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => val || undefined),
|
||||
rerankerEnabled: z.boolean().optional().default(false),
|
||||
rerankerModel: rerankerModelSchema.optional().default(DEFAULT_RERANKER_MODEL),
|
||||
/**
|
||||
* Number of vector results sent to Cohere as the documents array for reranking. Capped at 100
|
||||
* so each rerank call stays within a single Cohere search unit (1 query × ≤100 docs); see
|
||||
* `RERANK_MODEL_PRICING` in `providers/models.ts`.
|
||||
*/
|
||||
rerankerInputCount: z
|
||||
.number()
|
||||
.int('rerankerInputCount must be an integer')
|
||||
.min(1, 'rerankerInputCount must be at least 1')
|
||||
.max(100, 'rerankerInputCount cannot exceed 100')
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => val ?? undefined),
|
||||
rerankerApiKey: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => val || undefined),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const hasQuery = data.query && data.query.trim().length > 0
|
||||
const hasTagFilters = data.tagFilters && data.tagFilters.length > 0
|
||||
return hasQuery || hasTagFilters
|
||||
},
|
||||
{
|
||||
message: 'Please provide either a search query or tag filters to search your knowledge base',
|
||||
}
|
||||
)
|
||||
export type KnowledgeSearchBody = z.output<typeof knowledgeSearchBodySchema>
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Tests for shared knowledge contract schemas
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { knowledgeDocumentFileUrlSchema } from '@/lib/api/contracts/knowledge/shared'
|
||||
|
||||
describe('knowledgeDocumentFileUrlSchema', () => {
|
||||
it('accepts data: URIs', () => {
|
||||
const result = knowledgeDocumentFileUrlSchema.safeParse(
|
||||
'data:text/plain;base64,SGVsbG8gd29ybGQ='
|
||||
)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts https URLs', () => {
|
||||
const result = knowledgeDocumentFileUrlSchema.safeParse('https://example.com/file.pdf')
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts http URLs', () => {
|
||||
const result = knowledgeDocumentFileUrlSchema.safeParse(
|
||||
'http://localhost:3000/api/files/serve/kb/foo.pdf?context=knowledge-base'
|
||||
)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('is case-insensitive on the scheme', () => {
|
||||
expect(knowledgeDocumentFileUrlSchema.safeParse('HTTPS://example.com/x').success).toBe(true)
|
||||
expect(knowledgeDocumentFileUrlSchema.safeParse('Http://example.com/x').success).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['absolute local path', '/etc/passwd'],
|
||||
['app path', '/app/.env'],
|
||||
['relative path', './secrets.txt'],
|
||||
['parent traversal', '../../etc/shadow'],
|
||||
['file:// scheme', 'file:///etc/passwd'],
|
||||
['ftp scheme', 'ftp://example.com/x'],
|
||||
['javascript scheme', 'javascript:alert(1)'],
|
||||
['gopher scheme', 'gopher://example.com'],
|
||||
['relative serve path', '/api/files/serve/kb/foo.pdf'],
|
||||
['windows path', 'C:\\Windows\\System32\\config\\SAM'],
|
||||
['empty string', ''],
|
||||
['whitespace prefix', ' https://example.com/x'],
|
||||
])('rejects %s', (_label, value) => {
|
||||
const result = knowledgeDocumentFileUrlSchema.safeParse(value)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('returns a useful error message for unsupported schemes', () => {
|
||||
const result = knowledgeDocumentFileUrlSchema.safeParse('/etc/passwd')
|
||||
if (result.success) throw new Error('expected failure')
|
||||
expect(result.error.issues[0].message).toMatch(/data: URI or an http\(s\):\/\/ URL/)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const wireDateSchema = z.string()
|
||||
export const nullableWireDateSchema = z.string().nullable()
|
||||
|
||||
export const knowledgeBaseParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const knowledgeDocumentParamsSchema = knowledgeBaseParamsSchema.extend({
|
||||
documentId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const knowledgeChunkParamsSchema = knowledgeDocumentParamsSchema.extend({
|
||||
chunkId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const knowledgeTagParamsSchema = knowledgeBaseParamsSchema.extend({
|
||||
tagId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const knowledgeConnectorParamsSchema = knowledgeBaseParamsSchema.extend({
|
||||
connectorId: z.string().min(1),
|
||||
})
|
||||
|
||||
/**
|
||||
* A `fileUrl` accepted by knowledge document ingestion endpoints.
|
||||
*
|
||||
* Must be a `data:` URI or an `http(s)://` URL. Local paths, `file://`,
|
||||
* and other schemes are rejected at the boundary to prevent the background
|
||||
* parser from reading arbitrary files off the Sim server's filesystem.
|
||||
*/
|
||||
export const knowledgeDocumentFileUrlSchema = z
|
||||
.string()
|
||||
.min(1, 'File URL is required')
|
||||
.refine(
|
||||
(value) => /^data:/i.test(value) || /^https?:\/\//i.test(value),
|
||||
'File URL must be a data: URI or an http(s):// URL'
|
||||
)
|
||||
|
||||
export const documentTagFieldSchema = z.string().nullable().optional()
|
||||
export const documentNumberFieldSchema = z.number().nullable().optional()
|
||||
export const documentBooleanFieldSchema = z.boolean().nullable().optional()
|
||||
export const documentDateFieldSchema = nullableWireDateSchema.optional()
|
||||
|
||||
export const paginationSchema = z
|
||||
.object({
|
||||
total: z.number(),
|
||||
limit: z.number(),
|
||||
offset: z.number(),
|
||||
hasMore: z.boolean(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const successResponseSchema = <T extends z.ZodType>(dataSchema: T) =>
|
||||
z.object({
|
||||
success: z.literal(true),
|
||||
data: dataSchema,
|
||||
})
|
||||
@@ -0,0 +1,161 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
knowledgeBaseParamsSchema,
|
||||
knowledgeDocumentParamsSchema,
|
||||
knowledgeTagParamsSchema,
|
||||
successResponseSchema,
|
||||
} from '@/lib/api/contracts/knowledge/shared'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const nextAvailableSlotQuerySchema = z.object({
|
||||
fieldType: z.string().min(1),
|
||||
})
|
||||
|
||||
export const createTagDefinitionBodySchema = z.object({
|
||||
tagSlot: z.string().min(1, 'Tag slot is required'),
|
||||
displayName: z.string().min(1, 'Display name is required'),
|
||||
fieldType: z.string().min(1, 'Invalid field type'),
|
||||
})
|
||||
|
||||
export const documentTagDefinitionInputSchema = z.object({
|
||||
tagSlot: z.string().min(1, 'Tag slot is required'),
|
||||
displayName: z.string().min(1, 'Display name is required').max(100, 'Display name too long'),
|
||||
fieldType: z.string().default('text'),
|
||||
_originalDisplayName: z.string().optional(),
|
||||
})
|
||||
|
||||
export const saveDocumentTagDefinitionsBodySchema = z.object({
|
||||
definitions: z.array(documentTagDefinitionInputSchema),
|
||||
})
|
||||
|
||||
export const deleteDocumentTagDefinitionsQuerySchema = z.object({
|
||||
action: z.enum(['cleanup', 'all']).optional(),
|
||||
})
|
||||
|
||||
export const tagDefinitionDataSchema = z.object({
|
||||
id: z.string(),
|
||||
tagSlot: z.string(),
|
||||
displayName: z.string(),
|
||||
fieldType: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
|
||||
export type TagDefinitionData = z.output<typeof tagDefinitionDataSchema>
|
||||
export type DocumentTagDefinitionData = TagDefinitionData
|
||||
|
||||
export const nextAvailableSlotDataSchema = z.object({
|
||||
nextAvailableSlot: z.string().nullable(),
|
||||
fieldType: z.string(),
|
||||
usedSlots: z.array(z.string()),
|
||||
totalSlots: z.number(),
|
||||
availableSlots: z.number(),
|
||||
})
|
||||
export type NextAvailableSlotData = z.output<typeof nextAvailableSlotDataSchema>
|
||||
|
||||
export const saveDocumentTagDefinitionsDataSchema = z
|
||||
.object({
|
||||
created: z.array(tagDefinitionDataSchema).optional(),
|
||||
updated: z.array(tagDefinitionDataSchema).optional(),
|
||||
errors: z.array(z.string()).optional(),
|
||||
})
|
||||
.or(z.array(tagDefinitionDataSchema))
|
||||
export type SaveDocumentTagDefinitionsResult = z.output<typeof saveDocumentTagDefinitionsDataSchema>
|
||||
|
||||
export const tagUsageDocumentSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
tagValue: z.string(),
|
||||
})
|
||||
|
||||
export const tagUsageDataSchema = z.object({
|
||||
tagName: z.string(),
|
||||
tagSlot: z.string(),
|
||||
documentCount: z.number(),
|
||||
documents: z.array(tagUsageDocumentSchema),
|
||||
})
|
||||
|
||||
export type TagUsageData = z.output<typeof tagUsageDataSchema>
|
||||
|
||||
export const listTagDefinitionsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/knowledge/[id]/tag-definitions',
|
||||
params: knowledgeBaseParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(z.array(tagDefinitionDataSchema)),
|
||||
},
|
||||
})
|
||||
|
||||
export const createTagDefinitionContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/knowledge/[id]/tag-definitions',
|
||||
params: knowledgeBaseParamsSchema,
|
||||
body: createTagDefinitionBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(tagDefinitionDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteTagDefinitionContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/knowledge/[id]/tag-definitions/[tagId]',
|
||||
params: knowledgeTagParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({ success: z.literal(true) }).passthrough(),
|
||||
},
|
||||
})
|
||||
|
||||
export const nextAvailableSlotContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/knowledge/[id]/next-available-slot',
|
||||
params: knowledgeBaseParamsSchema,
|
||||
query: nextAvailableSlotQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(nextAvailableSlotDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const listDocumentTagDefinitionsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/knowledge/[id]/documents/[documentId]/tag-definitions',
|
||||
params: knowledgeDocumentParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(z.array(tagDefinitionDataSchema)),
|
||||
},
|
||||
})
|
||||
|
||||
export const saveDocumentTagDefinitionsContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/knowledge/[id]/documents/[documentId]/tag-definitions',
|
||||
params: knowledgeDocumentParamsSchema,
|
||||
body: saveDocumentTagDefinitionsBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(saveDocumentTagDefinitionsDataSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteDocumentTagDefinitionsContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/knowledge/[id]/documents/[documentId]/tag-definitions',
|
||||
params: knowledgeDocumentParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({ success: z.literal(true) }).passthrough(),
|
||||
},
|
||||
})
|
||||
|
||||
export const getTagUsageContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/knowledge/[id]/tag-usage',
|
||||
params: knowledgeBaseParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema(z.array(tagUsageDataSchema)),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,403 @@
|
||||
import { z } from 'zod'
|
||||
import { userFileSchema } from '@/lib/api/contracts/primitives'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const comparisonOperatorSchema = z.enum(['=', '>', '<', '>=', '<=', '!='])
|
||||
|
||||
export const logIdParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const executionIdParamsSchema = z.object({
|
||||
executionId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const cancelWorkflowExecutionParamsSchema = z.object({
|
||||
id: z.string().min(1, 'Invalid workflow ID'),
|
||||
executionId: z.string().min(1, 'Invalid execution ID'),
|
||||
})
|
||||
|
||||
const logFilterQuerySchema = z.object({
|
||||
workspaceId: z.string(),
|
||||
level: z.string().optional(),
|
||||
workflowIds: z.string().optional(),
|
||||
folderIds: z.string().optional(),
|
||||
triggers: z.string().optional(),
|
||||
startDate: z.string().optional(),
|
||||
endDate: z.string().optional(),
|
||||
search: z.string().optional(),
|
||||
workflowName: z.string().optional(),
|
||||
folderName: z.string().optional(),
|
||||
executionId: z.string().optional(),
|
||||
costOperator: comparisonOperatorSchema.optional(),
|
||||
costValue: z.coerce.number().optional(),
|
||||
durationOperator: comparisonOperatorSchema.optional(),
|
||||
durationValue: z.coerce.number().optional(),
|
||||
})
|
||||
|
||||
export const logSortBySchema = z.enum(['date', 'duration', 'cost', 'status']).default('date')
|
||||
export const logSortOrderSchema = z.enum(['asc', 'desc']).default('desc')
|
||||
|
||||
export const listLogsQuerySchema = logFilterQuerySchema.extend({
|
||||
cursor: z.string().optional(),
|
||||
limit: z.coerce.number().int().min(1).max(200).optional().default(100),
|
||||
sortBy: logSortBySchema,
|
||||
sortOrder: logSortOrderSchema,
|
||||
})
|
||||
|
||||
export const logDetailQuerySchema = z.object({
|
||||
workspaceId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const statsQueryParamsSchema = logFilterQuerySchema.extend({
|
||||
segmentCount: z.coerce.number().optional().default(72),
|
||||
})
|
||||
|
||||
const workflowSummarySchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullable(),
|
||||
description: z.string().nullable(),
|
||||
folderId: z.string().nullable(),
|
||||
userId: z.string().nullable(),
|
||||
workspaceId: z.string().nullable(),
|
||||
createdAt: z.string().nullable(),
|
||||
updatedAt: z.string().nullable(),
|
||||
})
|
||||
.partial()
|
||||
|
||||
const tokenBreakdownSchema = z
|
||||
.object({
|
||||
total: z.number().optional(),
|
||||
input: z.number().optional(),
|
||||
output: z.number().optional(),
|
||||
prompt: z.number().optional(),
|
||||
completion: z.number().optional(),
|
||||
})
|
||||
.partial()
|
||||
|
||||
const modelCostSchema = z
|
||||
.object({
|
||||
input: z.number().optional(),
|
||||
output: z.number().optional(),
|
||||
total: z.number().optional(),
|
||||
tokens: tokenBreakdownSchema.optional(),
|
||||
})
|
||||
.partial()
|
||||
|
||||
const costSummarySchema = z
|
||||
.object({
|
||||
total: z.number().optional(),
|
||||
input: z.number().optional(),
|
||||
output: z.number().optional(),
|
||||
tokens: tokenBreakdownSchema.optional(),
|
||||
models: z.record(z.string(), modelCostSchema).optional(),
|
||||
pricing: z
|
||||
.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
cachedInput: z.number().optional(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.partial()
|
||||
|
||||
/**
|
||||
* Itemized cost breakdown derived from the usage_log ledger (the single source
|
||||
* of truth) for the detail view. Each item is one billed line (base fee, a
|
||||
* model, or a tool/integration); the items reconcile to `total`.
|
||||
*/
|
||||
const costLedgerItemSchema = z.object({
|
||||
category: z.enum(['fixed', 'model', 'tool']),
|
||||
description: z.string(),
|
||||
cost: z.number(),
|
||||
inputTokens: z.number().optional(),
|
||||
outputTokens: z.number().optional(),
|
||||
})
|
||||
|
||||
export const costLedgerSchema = z.object({
|
||||
total: z.number(),
|
||||
items: z.array(costLedgerItemSchema),
|
||||
})
|
||||
|
||||
export type CostLedger = z.output<typeof costLedgerSchema>
|
||||
export type CostLedgerItem = z.output<typeof costLedgerItemSchema>
|
||||
|
||||
const pauseSummarySchema = z.object({
|
||||
status: z.string().nullable(),
|
||||
total: z.number(),
|
||||
resumed: z.number(),
|
||||
})
|
||||
|
||||
const blockExecutionSchema = z.object({
|
||||
id: z.string(),
|
||||
blockId: z.string(),
|
||||
blockName: z.string(),
|
||||
blockType: z.string(),
|
||||
startedAt: z.string(),
|
||||
endedAt: z.string(),
|
||||
durationMs: z.number(),
|
||||
status: z.enum(['success', 'error', 'skipped']),
|
||||
errorMessage: z.string().optional(),
|
||||
errorStackTrace: z.string().optional(),
|
||||
inputData: z.unknown(),
|
||||
outputData: z.unknown(),
|
||||
cost: costSummarySchema.optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
|
||||
const toolCallSchema = z
|
||||
.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
arguments: z.unknown().optional(),
|
||||
result: z.unknown().optional(),
|
||||
error: z.string().optional(),
|
||||
startTime: z.string().optional(),
|
||||
endTime: z.string().optional(),
|
||||
duration: z.number().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
type TraceSpan = {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
duration?: number
|
||||
durationMs?: number
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
status?: string
|
||||
blockId?: string
|
||||
input?: unknown
|
||||
output?: unknown
|
||||
tokens?: number | { total?: number; input?: number; output?: number }
|
||||
relativeStartMs?: number
|
||||
toolCalls?: Array<z.output<typeof toolCallSchema>>
|
||||
children?: TraceSpan[]
|
||||
}
|
||||
|
||||
const traceSpanSchema: z.ZodType<TraceSpan> = z.lazy(() =>
|
||||
z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
duration: z.number().optional(),
|
||||
durationMs: z.number().optional(),
|
||||
startTime: z.string().optional(),
|
||||
endTime: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
blockId: z.string().optional(),
|
||||
input: z.unknown().optional(),
|
||||
output: z.unknown().optional(),
|
||||
tokens: z
|
||||
.union([
|
||||
z.number(),
|
||||
z
|
||||
.object({
|
||||
total: z.number().optional(),
|
||||
input: z.number().optional(),
|
||||
output: z.number().optional(),
|
||||
})
|
||||
.partial(),
|
||||
])
|
||||
.optional(),
|
||||
relativeStartMs: z.number().optional(),
|
||||
toolCalls: z.array(toolCallSchema).optional(),
|
||||
children: z.array(traceSpanSchema).optional(),
|
||||
})
|
||||
.passthrough()
|
||||
)
|
||||
|
||||
const executionDataDetailSchema = z
|
||||
.object({
|
||||
totalDuration: z.number().nullable().optional(),
|
||||
enhanced: z.literal(true).optional(),
|
||||
traceSpans: z.array(traceSpanSchema).optional(),
|
||||
blockExecutions: z.array(blockExecutionSchema).optional(),
|
||||
finalOutput: z.unknown().optional(),
|
||||
workflowInput: z.unknown().optional(),
|
||||
blockInput: z.record(z.string(), z.unknown()).optional(),
|
||||
trigger: z.unknown().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const workflowLogSummarySchema = z.object({
|
||||
id: z.string(),
|
||||
workflowId: z.string().nullable(),
|
||||
executionId: z.string().nullable(),
|
||||
deploymentVersionId: z.string().nullable(),
|
||||
deploymentVersion: z.number().nullable(),
|
||||
deploymentVersionName: z.string().nullable(),
|
||||
level: z.string(),
|
||||
status: z.string().nullable(),
|
||||
duration: z.string().nullable(),
|
||||
trigger: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
workflow: workflowSummarySchema.nullable(),
|
||||
jobTitle: z.string().nullable(),
|
||||
// Top-level run cost is the cost_total projection of the usage_log ledger,
|
||||
// rendered as { total } (dollars). The itemized breakdown lives in costLedger
|
||||
// (detail only); per-block costs use the richer costSummarySchema elsewhere.
|
||||
cost: z.object({ total: z.number() }).nullable(),
|
||||
pauseSummary: pauseSummarySchema,
|
||||
hasPendingPause: z.boolean(),
|
||||
})
|
||||
|
||||
export const workflowLogDetailSchema = workflowLogSummarySchema.extend({
|
||||
executionData: executionDataDetailSchema,
|
||||
files: z.array(userFileSchema).nullable(),
|
||||
// Itemized, ledger-sourced cost breakdown. Null for legacy/pre-ledger runs,
|
||||
// where the UI falls back to the (reconciling) cost jsonb.
|
||||
costLedger: costLedgerSchema.nullable().optional(),
|
||||
})
|
||||
|
||||
export type WorkflowLogSummary = z.output<typeof workflowLogSummarySchema>
|
||||
export type WorkflowLogDetail = z.output<typeof workflowLogDetailSchema>
|
||||
|
||||
/**
|
||||
* A row that may be either a list-view summary or a fully loaded detail. Used by
|
||||
* UI surfaces that render the same log before and after its detail query resolves.
|
||||
*/
|
||||
export type WorkflowLogRow = WorkflowLogSummary &
|
||||
Partial<Pick<WorkflowLogDetail, 'executionData' | 'files' | 'costLedger'>>
|
||||
|
||||
export const listLogsResponseSchema = z.object({
|
||||
data: z.array(workflowLogSummarySchema),
|
||||
nextCursor: z.string().nullable(),
|
||||
})
|
||||
|
||||
export type ListLogsResponse = z.output<typeof listLogsResponseSchema>
|
||||
|
||||
export const segmentStatsSchema = z.object({
|
||||
timestamp: z.string(),
|
||||
totalExecutions: z.number(),
|
||||
successfulExecutions: z.number(),
|
||||
avgDurationMs: z.number(),
|
||||
})
|
||||
|
||||
export const workflowStatsSchema = z.object({
|
||||
workflowId: z.string(),
|
||||
workflowName: z.string(),
|
||||
segments: z.array(segmentStatsSchema),
|
||||
overallSuccessRate: z.number(),
|
||||
totalExecutions: z.number(),
|
||||
totalSuccessful: z.number(),
|
||||
})
|
||||
|
||||
export const dashboardStatsResponseSchema = z.object({
|
||||
workflows: z.array(workflowStatsSchema),
|
||||
aggregateSegments: z.array(segmentStatsSchema),
|
||||
totalRuns: z.number(),
|
||||
totalErrors: z.number(),
|
||||
avgLatency: z.number(),
|
||||
timeBounds: z.object({
|
||||
start: z.string(),
|
||||
end: z.string(),
|
||||
}),
|
||||
segmentMs: z.number(),
|
||||
})
|
||||
|
||||
export const executionSnapshotDataSchema = z.object({
|
||||
executionId: z.string(),
|
||||
workflowId: z.string().nullable(),
|
||||
workflowState: z.record(z.string(), z.unknown()).nullable(),
|
||||
childWorkflowSnapshots: z.record(z.string(), z.unknown()).optional(),
|
||||
executionMetadata: z.object({
|
||||
trigger: z.string().nullable(),
|
||||
startedAt: z.string(),
|
||||
endedAt: z.string().optional(),
|
||||
totalDurationMs: z.number().nullable().optional(),
|
||||
cost: z.unknown().nullable(),
|
||||
totalTokens: z.number().nullable().optional(),
|
||||
}),
|
||||
})
|
||||
|
||||
export const triggersQuerySchema = z.object({
|
||||
workspaceId: z.string(),
|
||||
})
|
||||
export type TriggersQuery = z.output<typeof triggersQuerySchema>
|
||||
|
||||
export const cancelWorkflowExecutionResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
executionId: z.string(),
|
||||
redisAvailable: z.boolean(),
|
||||
durablyRecorded: z.boolean(),
|
||||
locallyAborted: z.boolean(),
|
||||
pausedCancelled: z.boolean(),
|
||||
reason: z.enum(['recorded', 'redis_unavailable', 'redis_write_failed']),
|
||||
})
|
||||
|
||||
export type SegmentStats = z.output<typeof segmentStatsSchema>
|
||||
export type WorkflowStats = z.output<typeof workflowStatsSchema>
|
||||
export type DashboardStatsResponse = z.output<typeof dashboardStatsResponseSchema>
|
||||
export type ExecutionSnapshotData = z.output<typeof executionSnapshotDataSchema>
|
||||
export type CancelWorkflowExecutionResponse = z.output<typeof cancelWorkflowExecutionResponseSchema>
|
||||
|
||||
export const listLogsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/logs',
|
||||
query: listLogsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: listLogsResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getLogDetailContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/logs/[id]',
|
||||
params: logIdParamsSchema,
|
||||
query: logDetailQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
data: workflowLogDetailSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const getLogByExecutionIdContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/logs/by-execution/[executionId]',
|
||||
params: executionIdParamsSchema,
|
||||
query: logDetailQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
data: workflowLogDetailSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const getDashboardStatsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/logs/stats',
|
||||
query: statsQueryParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: dashboardStatsResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getExecutionSnapshotContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/logs/execution/[executionId]',
|
||||
params: executionIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: executionSnapshotDataSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const cancelWorkflowExecutionContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/workflows/[id]/executions/[executionId]/cancel',
|
||||
params: cancelWorkflowExecutionParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: cancelWorkflowExecutionResponseSchema,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const mcpOauthMetadataQuerySchema = z.record(z.string(), z.string())
|
||||
export type McpOauthMetadataQuery = z.input<typeof mcpOauthMetadataQuerySchema>
|
||||
|
||||
const xSimAuthSchema = z.object({
|
||||
type: z.literal('api_key'),
|
||||
header: z.string(),
|
||||
})
|
||||
|
||||
const mcpAuthorizationServerMetadataSchema = z.object({
|
||||
issuer: z.string(),
|
||||
authorization_endpoint: z.string(),
|
||||
token_endpoint: z.string(),
|
||||
registration_endpoint: z.string(),
|
||||
jwks_uri: z.string(),
|
||||
token_endpoint_auth_methods_supported: z.array(z.string()),
|
||||
grant_types_supported: z.array(z.string()),
|
||||
response_types_supported: z.array(z.string()),
|
||||
code_challenge_methods_supported: z.array(z.string()),
|
||||
scopes_supported: z.array(z.string()),
|
||||
resource: z.string(),
|
||||
x_sim_auth: xSimAuthSchema,
|
||||
})
|
||||
export type McpAuthorizationServerMetadata = z.output<typeof mcpAuthorizationServerMetadataSchema>
|
||||
|
||||
const mcpProtectedResourceMetadataSchema = z.object({
|
||||
resource: z.string(),
|
||||
authorization_servers: z.array(z.string()),
|
||||
bearer_methods_supported: z.array(z.string()),
|
||||
scopes_supported: z.array(z.string()),
|
||||
x_sim_auth: xSimAuthSchema,
|
||||
})
|
||||
export type McpProtectedResourceMetadata = z.output<typeof mcpProtectedResourceMetadataSchema>
|
||||
|
||||
export const mcpOauthAuthorizationServerMetadataContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/mcp/copilot/.well-known/oauth-authorization-server',
|
||||
query: mcpOauthMetadataQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: mcpAuthorizationServerMetadataSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const mcpOauthProtectedResourceMetadataContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/mcp/copilot/.well-known/oauth-protected-resource',
|
||||
query: mcpOauthMetadataQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: mcpProtectedResourceMetadataSchema,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,501 @@
|
||||
import { z } from 'zod'
|
||||
import { type ContractJsonResponse, defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import type { McpToolSchema, McpToolSchemaProperty } from '@/lib/mcp/types'
|
||||
|
||||
const MAX_MCP_REFRESH_SERVER_IDS = 100
|
||||
|
||||
const dateStringSchema = z.preprocess(
|
||||
(value) => (value instanceof Date ? value.toISOString() : value),
|
||||
z.string()
|
||||
)
|
||||
|
||||
const optionalStringFromNullableSchema = z.preprocess(
|
||||
(value) => (value === null ? undefined : value),
|
||||
z.string().optional()
|
||||
)
|
||||
|
||||
const optionalDateStringFromNullableSchema = z.preprocess((value) => {
|
||||
if (value instanceof Date) return value.toISOString()
|
||||
return value === null ? undefined : value
|
||||
}, z.string().optional())
|
||||
|
||||
const optionalNumberFromNullableSchema = z.preprocess(
|
||||
(value) => (value === null ? undefined : value),
|
||||
z.number().optional()
|
||||
)
|
||||
|
||||
const optionalConnectionStatusFromNullableSchema = z.preprocess(
|
||||
(value) => (value === null ? undefined : value),
|
||||
z.enum(['connected', 'disconnected', 'error']).optional()
|
||||
)
|
||||
|
||||
const optionalHeadersFromNullableSchema = z.preprocess(
|
||||
(value) => (value === null ? undefined : value),
|
||||
z.record(z.string(), z.string()).optional()
|
||||
)
|
||||
|
||||
export const mcpTransportSchema = z.enum(['streamable-http'])
|
||||
|
||||
export const mcpAuthTypeSchema = z.enum(['none', 'headers', 'oauth'])
|
||||
|
||||
const consecutiveFailuresSchema = z.preprocess(
|
||||
(value) => (typeof value === 'number' ? value : undefined),
|
||||
z.number().default(0)
|
||||
)
|
||||
|
||||
export const mcpServerStatusConfigSchema = z
|
||||
.object({
|
||||
consecutiveFailures: consecutiveFailuresSchema,
|
||||
lastSuccessfulDiscovery: z.string().nullable().default(null),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const mcpToolSchemaPropertySchema: z.ZodType<McpToolSchemaProperty> = z.lazy(() =>
|
||||
z
|
||||
.object({
|
||||
type: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
description: z.string().optional(),
|
||||
items: z
|
||||
.union([mcpToolSchemaPropertySchema, z.array(mcpToolSchemaPropertySchema)])
|
||||
.optional(),
|
||||
properties: z.record(z.string(), mcpToolSchemaPropertySchema).optional(),
|
||||
required: z.array(z.string()).optional(),
|
||||
enum: z.array(z.unknown()).optional(),
|
||||
default: z.unknown().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
)
|
||||
|
||||
export const mcpToolInputSchema: z.ZodType<McpToolSchema> = z
|
||||
.object({
|
||||
type: z.literal('object'),
|
||||
properties: z.record(z.string(), mcpToolSchemaPropertySchema).optional(),
|
||||
required: z.array(z.string()).optional(),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const mcpToolSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
inputSchema: mcpToolInputSchema,
|
||||
serverId: z.string(),
|
||||
serverName: z.string(),
|
||||
})
|
||||
|
||||
export const storedMcpToolSchema = z.object({
|
||||
workflowId: z.string(),
|
||||
workflowName: z.string(),
|
||||
serverId: z.string(),
|
||||
serverUrl: z.string().optional(),
|
||||
toolName: z.string(),
|
||||
schema: mcpToolInputSchema.optional(),
|
||||
})
|
||||
|
||||
export const mcpServerSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
workspaceId: z.string(),
|
||||
name: z.string(),
|
||||
description: optionalStringFromNullableSchema,
|
||||
transport: mcpTransportSchema,
|
||||
authType: mcpAuthTypeSchema.optional(),
|
||||
url: optionalStringFromNullableSchema,
|
||||
timeout: optionalNumberFromNullableSchema,
|
||||
retries: optionalNumberFromNullableSchema,
|
||||
headers: optionalHeadersFromNullableSchema,
|
||||
enabled: z.boolean(),
|
||||
connectionStatus: optionalConnectionStatusFromNullableSchema,
|
||||
lastError: z.string().nullable().optional(),
|
||||
statusConfig: z.preprocess(
|
||||
(value) => (value === null ? undefined : value),
|
||||
mcpServerStatusConfigSchema.optional()
|
||||
),
|
||||
toolCount: optionalNumberFromNullableSchema,
|
||||
lastToolsRefresh: optionalDateStringFromNullableSchema,
|
||||
lastConnected: optionalDateStringFromNullableSchema,
|
||||
createdAt: dateStringSchema,
|
||||
updatedAt: dateStringSchema,
|
||||
deletedAt: optionalDateStringFromNullableSchema,
|
||||
oauthClientId: optionalStringFromNullableSchema,
|
||||
hasOauthClientSecret: z.boolean().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
export type McpServer = z.output<typeof mcpServerSchema>
|
||||
|
||||
export const mcpWorkspaceQuerySchema = z.object({
|
||||
workspaceId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const mcpServerIdParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const createMcpServerBodySchema = z
|
||||
.object({
|
||||
name: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
transport: mcpTransportSchema,
|
||||
url: z.string().optional(),
|
||||
authType: mcpAuthTypeSchema.optional(),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
timeout: z.number().optional(),
|
||||
retries: z.number().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
source: z.string().optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
oauthClientId: z.string().nullable().optional(),
|
||||
oauthClientSecret: z.string().nullable().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const updateMcpServerBodySchema = createMcpServerBodySchema.partial()
|
||||
|
||||
export const deleteMcpServerQuerySchema = mcpWorkspaceQuerySchema.extend({
|
||||
serverId: z.string().min(1),
|
||||
source: z.string().optional(),
|
||||
})
|
||||
|
||||
export const deleteMcpServerByQuerySchema = z.object({
|
||||
serverId: z.string().optional(),
|
||||
source: z.string().optional(),
|
||||
})
|
||||
|
||||
export const discoverMcpToolsQuerySchema = mcpWorkspaceQuerySchema.extend({
|
||||
serverId: z.string().optional(),
|
||||
refresh: z
|
||||
.union([z.boolean(), z.enum(['true', 'false']).transform((value) => value === 'true')])
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const refreshMcpToolsBodySchema = z.object({
|
||||
serverIds: z
|
||||
.array(z.string().min(1))
|
||||
.transform((serverIds) => [...new Set(serverIds)])
|
||||
.pipe(
|
||||
z
|
||||
.array(z.string())
|
||||
.max(
|
||||
MAX_MCP_REFRESH_SERVER_IDS,
|
||||
`At most ${MAX_MCP_REFRESH_SERVER_IDS} MCP servers can be refreshed at once`
|
||||
)
|
||||
),
|
||||
})
|
||||
|
||||
export const mcpEventsQuerySchema = z.object({
|
||||
workspaceId: z.string().min(1).nullable(),
|
||||
})
|
||||
|
||||
export const mcpServeRouteParamsSchema = z.object({
|
||||
serverId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const mcpToolDiscoveryQuerySchema = z.object({
|
||||
serverId: z.string().optional(),
|
||||
refresh: z.string().optional(),
|
||||
})
|
||||
|
||||
export const mcpToolExecutionBodySchema = z
|
||||
.object({
|
||||
serverId: z.string().min(1),
|
||||
toolName: z.string().min(1),
|
||||
arguments: z.record(z.string(), z.unknown()).optional(),
|
||||
workflowId: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
export type McpToolExecutionBody = z.input<typeof mcpToolExecutionBodySchema>
|
||||
|
||||
export const mcpToolResultSchema = z
|
||||
.object({
|
||||
content: z.array(z.unknown()).optional(),
|
||||
isError: z.boolean().optional(),
|
||||
structuredContent: z.unknown().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const mcpToolExecutionResultSchema = z.object({
|
||||
success: z.boolean(),
|
||||
output: mcpToolResultSchema.optional(),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
export type McpToolExecutionResult = z.output<typeof mcpToolExecutionResultSchema>
|
||||
|
||||
export const mcpJsonRpcRequestSchema = z
|
||||
.object({
|
||||
jsonrpc: z.literal('2.0'),
|
||||
id: z.union([z.string(), z.number()]),
|
||||
method: z.string().min(1),
|
||||
params: z.unknown().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const mcpJsonRpcNotificationSchema = z
|
||||
.object({
|
||||
jsonrpc: z.literal('2.0'),
|
||||
method: z.string().min(1),
|
||||
params: z.unknown().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const mcpJsonRpcMessageSchema = z
|
||||
.object({
|
||||
jsonrpc: z.literal('2.0'),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const mcpRequestBodySchema = z.union([
|
||||
mcpJsonRpcMessageSchema,
|
||||
z.array(mcpJsonRpcMessageSchema),
|
||||
])
|
||||
|
||||
export const mcpToolCallParamsSchema = z
|
||||
.object({
|
||||
name: z.string().min(1),
|
||||
arguments: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const mcpServerTestBodySchema = z
|
||||
.object({
|
||||
name: z.string().min(1),
|
||||
transport: mcpTransportSchema,
|
||||
url: z.string().optional(),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
timeout: z.number().optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
export type McpServerTestBody = z.input<typeof mcpServerTestBodySchema>
|
||||
|
||||
export const mcpServerTestResultSchema = z.object({
|
||||
success: z.boolean(),
|
||||
message: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
authRequired: z.boolean().optional(),
|
||||
authType: mcpAuthTypeSchema.optional(),
|
||||
serverInfo: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
version: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
negotiatedVersion: z.string().optional(),
|
||||
supportedCapabilities: z.array(z.string()).optional(),
|
||||
toolCount: z.number().optional(),
|
||||
warnings: z.array(z.string()).optional(),
|
||||
})
|
||||
export type McpServerTestResult = z.output<typeof mcpServerTestResultSchema>
|
||||
|
||||
export const refreshMcpServerResultSchema = z.object({
|
||||
status: z.enum(['connected', 'disconnected', 'error']),
|
||||
toolCount: z.number(),
|
||||
lastConnected: z.string().nullable(),
|
||||
error: z.string().nullable(),
|
||||
workflowsUpdated: z.number(),
|
||||
updatedWorkflowIds: z.array(z.string()),
|
||||
})
|
||||
export type RefreshMcpServerResult = z.output<typeof refreshMcpServerResultSchema>
|
||||
|
||||
const mcpSuccessResponseSchema = <T extends z.ZodType>(dataSchema: T) =>
|
||||
z.object({
|
||||
success: z.literal(true),
|
||||
data: dataSchema,
|
||||
})
|
||||
|
||||
export const listMcpServersContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/mcp/servers',
|
||||
query: mcpWorkspaceQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: mcpSuccessResponseSchema(
|
||||
z.object({
|
||||
servers: z.array(mcpServerSchema),
|
||||
})
|
||||
),
|
||||
},
|
||||
})
|
||||
export type ListMcpServersResponse = ContractJsonResponse<typeof listMcpServersContract>
|
||||
|
||||
export const createMcpServerContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/mcp/servers',
|
||||
body: createMcpServerBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: mcpSuccessResponseSchema(
|
||||
z.object({
|
||||
serverId: z.string(),
|
||||
updated: z.boolean().optional(),
|
||||
authType: mcpAuthTypeSchema.optional(),
|
||||
})
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteMcpServerContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/mcp/servers',
|
||||
query: deleteMcpServerQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: mcpSuccessResponseSchema(
|
||||
z.object({
|
||||
message: z.string(),
|
||||
})
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateMcpServerContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/mcp/servers/[id]',
|
||||
params: mcpServerIdParamsSchema,
|
||||
query: mcpWorkspaceQuerySchema,
|
||||
body: updateMcpServerBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: mcpSuccessResponseSchema(
|
||||
z.object({
|
||||
server: mcpServerSchema,
|
||||
})
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
export const refreshMcpServerContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/mcp/servers/[id]/refresh',
|
||||
params: mcpServerIdParamsSchema,
|
||||
query: mcpWorkspaceQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: mcpSuccessResponseSchema(refreshMcpServerResultSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const discoverMcpToolsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/mcp/tools/discover',
|
||||
query: discoverMcpToolsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: mcpSuccessResponseSchema(
|
||||
z.object({
|
||||
tools: z.array(mcpToolSchema),
|
||||
totalCount: z.number(),
|
||||
byServer: z.record(z.string(), z.number()),
|
||||
})
|
||||
),
|
||||
},
|
||||
})
|
||||
export type DiscoverMcpToolsResponse = ContractJsonResponse<typeof discoverMcpToolsContract>
|
||||
|
||||
export const refreshMcpToolsContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/mcp/tools/discover',
|
||||
query: mcpWorkspaceQuerySchema,
|
||||
body: refreshMcpToolsBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: mcpSuccessResponseSchema(
|
||||
z.object({
|
||||
refreshed: z.array(z.object({ serverId: z.string(), toolCount: z.number() })),
|
||||
failed: z.array(z.object({ serverId: z.string(), error: z.string() })),
|
||||
summary: z.object({
|
||||
total: z.number(),
|
||||
successful: z.number(),
|
||||
failed: z.number(),
|
||||
}),
|
||||
})
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
export const listStoredMcpToolsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/mcp/tools/stored',
|
||||
query: mcpWorkspaceQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: mcpSuccessResponseSchema(
|
||||
z.object({
|
||||
tools: z.array(storedMcpToolSchema),
|
||||
})
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
export const testMcpServerConnectionContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/mcp/servers/test-connection',
|
||||
body: mcpServerTestBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: mcpSuccessResponseSchema(mcpServerTestResultSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const executeMcpToolContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/mcp/tools/execute',
|
||||
body: mcpToolExecutionBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: mcpSuccessResponseSchema(mcpToolExecutionResultSchema),
|
||||
},
|
||||
})
|
||||
export type ExecuteMcpToolResponse = ContractJsonResponse<typeof executeMcpToolContract>
|
||||
|
||||
export const startMcpOauthQuerySchema = z.object({
|
||||
serverId: z.string().min(1, 'serverId is required'),
|
||||
workspaceId: z.string().min(1, 'workspaceId is required'),
|
||||
})
|
||||
|
||||
export const startMcpOauthResultSchema = z.discriminatedUnion('status', [
|
||||
z.object({ status: z.literal('redirect'), authorizationUrl: z.string().url() }),
|
||||
z.object({ status: z.literal('already_authorized') }),
|
||||
])
|
||||
export type StartMcpOauthResult = z.output<typeof startMcpOauthResultSchema>
|
||||
|
||||
export const startMcpOauthContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/mcp/oauth/start',
|
||||
query: startMcpOauthQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: startMcpOauthResultSchema,
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Provider can return any subset depending on the outcome:
|
||||
* - success: `state` + `code`
|
||||
* - provider error: `error` + optional `error_description` + optional `state`
|
||||
* - malformed callback: nothing
|
||||
* All fields are optional so the route can render an HTML error page itself.
|
||||
*/
|
||||
export const mcpOauthCallbackQuerySchema = z.object({
|
||||
state: z.string().optional(),
|
||||
code: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
error_description: z.string().optional(),
|
||||
})
|
||||
|
||||
export const mcpOauthCallbackContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/mcp/oauth/callback',
|
||||
query: mcpOauthCallbackQuerySchema,
|
||||
response: { mode: 'text' },
|
||||
})
|
||||
|
||||
export const getAllowedMcpDomainsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/settings/allowed-mcp-domains',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
allowedMcpDomains: z.array(z.string()).nullable(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from '@/lib/api/contracts/media/speech'
|
||||
export * from '@/lib/api/contracts/media/tts-stream'
|
||||
@@ -0,0 +1,21 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const speechTokenBodySchema = z
|
||||
.object({
|
||||
chatId: z.string().optional(),
|
||||
/** Editor/workspace voice: the workspace the session user is recording in. */
|
||||
workspaceId: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const speechTokenResponseSchema = z.object({
|
||||
token: z.string(),
|
||||
})
|
||||
|
||||
export const speechTokenContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/speech/token',
|
||||
body: speechTokenBodySchema,
|
||||
response: { mode: 'json', schema: speechTokenResponseSchema },
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const ttsStreamBodySchema = z
|
||||
.object({
|
||||
text: z.string().min(1),
|
||||
voiceId: z.string().min(1),
|
||||
modelId: z.string().optional().default('eleven_flash_v2_5'),
|
||||
chatId: z.string().min(1),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const ttsStreamContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/proxy/tts/stream',
|
||||
body: ttsStreamBodySchema,
|
||||
response: { mode: 'stream' },
|
||||
})
|
||||
@@ -0,0 +1,142 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const memoryIdParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const memoryWorkspaceQuerySchema = z.object({
|
||||
workspaceId: z.string().uuid('Invalid workspace ID format'),
|
||||
})
|
||||
|
||||
const agentMemoryDataSchema = z.object({
|
||||
role: z.enum(['user', 'assistant', 'system'], {
|
||||
error: 'Role must be user, assistant, or system',
|
||||
}),
|
||||
content: z.string().min(1, 'Content is required'),
|
||||
})
|
||||
|
||||
const genericMemoryDataSchema = z.record(z.string(), z.unknown())
|
||||
|
||||
export const memoryPutBodySchema = z.object({
|
||||
data: z.union([agentMemoryDataSchema, genericMemoryDataSchema], {
|
||||
error: 'Invalid memory data structure',
|
||||
}),
|
||||
workspaceId: z.string().uuid('Invalid workspace ID format'),
|
||||
})
|
||||
export type MemoryPutBody = z.input<typeof memoryPutBodySchema>
|
||||
|
||||
export const agentMemoryDataSchemaContract = agentMemoryDataSchema
|
||||
|
||||
export const memoryListQuerySchema = z.object({
|
||||
workspaceId: z.string().optional(),
|
||||
query: z.string().nullable().optional(),
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) => Number.parseInt(value || '50')),
|
||||
})
|
||||
|
||||
export const memoryMessageSchema = z
|
||||
.object({
|
||||
role: z.enum(['user', 'assistant', 'system']),
|
||||
content: z.unknown().refine((value) => Boolean(value)),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const memoryPostBodySchema = z
|
||||
.object({
|
||||
key: z.string().optional(),
|
||||
data: z.unknown().optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
export type MemoryPostBody = z.input<typeof memoryPostBodySchema>
|
||||
|
||||
export const memoryDeleteQuerySchema = z.object({
|
||||
workspaceId: z.string().optional(),
|
||||
conversationId: z.string().optional(),
|
||||
})
|
||||
|
||||
const memorySuccessResponseSchema = <T extends z.ZodType>(dataSchema: T) =>
|
||||
z.object({
|
||||
success: z.literal(true),
|
||||
data: dataSchema,
|
||||
})
|
||||
|
||||
const memoryRecordSchema = z.object({
|
||||
conversationId: z.string(),
|
||||
data: z.unknown(),
|
||||
})
|
||||
|
||||
export const listMemoriesContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/memory',
|
||||
query: memoryListQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: memorySuccessResponseSchema(
|
||||
z.object({
|
||||
memories: z.array(memoryRecordSchema),
|
||||
})
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
export const createMemoryContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/memory',
|
||||
body: memoryPostBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: memorySuccessResponseSchema(memoryRecordSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteMemoryByQueryContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/memory',
|
||||
query: memoryDeleteQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: memorySuccessResponseSchema(
|
||||
z.object({
|
||||
message: z.string(),
|
||||
deletedCount: z.number(),
|
||||
})
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
export const getMemoryByIdContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/memory/[id]',
|
||||
params: memoryIdParamsSchema,
|
||||
query: memoryWorkspaceQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: memorySuccessResponseSchema(memoryRecordSchema),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteMemoryByIdContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/memory/[id]',
|
||||
params: memoryIdParamsSchema,
|
||||
query: memoryWorkspaceQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: memorySuccessResponseSchema(z.object({ message: z.string() })),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateMemoryByIdContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/memory/[id]',
|
||||
params: memoryIdParamsSchema,
|
||||
body: memoryPutBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: memorySuccessResponseSchema(memoryRecordSchema),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,359 @@
|
||||
import { z } from 'zod'
|
||||
import { scheduleContextSchema } from '@/lib/api/contracts/schedules'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const dateStringSchema = z.string().refine((value) => !Number.isNaN(Date.parse(value)), {
|
||||
message: 'Expected a valid date string',
|
||||
})
|
||||
|
||||
export const listMothershipChatsQuerySchema = z.object({
|
||||
workspaceId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const mothershipChatParamsSchema = z.object({
|
||||
chatId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const updateMothershipChatBodySchema = z
|
||||
.object({
|
||||
title: z.string().trim().min(1).max(200).optional(),
|
||||
isUnread: z.boolean().optional(),
|
||||
pinned: z.boolean().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => data.title !== undefined || data.isUnread !== undefined || data.pinned !== undefined,
|
||||
{
|
||||
message: 'At least one field must be provided',
|
||||
}
|
||||
)
|
||||
|
||||
export const createMothershipChatBodySchema = z.object({
|
||||
workspaceId: z.string().min(1),
|
||||
})
|
||||
export type CreateMothershipChatBody = z.input<typeof createMothershipChatBodySchema>
|
||||
|
||||
export const markMothershipChatReadBodySchema = z.object({
|
||||
chatId: z.string().min(1),
|
||||
})
|
||||
export type MarkMothershipChatReadBody = z.input<typeof markMothershipChatReadBodySchema>
|
||||
|
||||
export const markMothershipChatReadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/mothership/chats/read',
|
||||
body: markMothershipChatReadBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const mothershipExecuteMessageSchema = z.object({
|
||||
role: z.enum(['system', 'user', 'assistant']),
|
||||
content: z.string(),
|
||||
})
|
||||
|
||||
const mothershipExecuteFileAttachmentSchema = z
|
||||
.object({
|
||||
type: z.enum(['text', 'image', 'document', 'audio', 'video']),
|
||||
source: z
|
||||
.object({
|
||||
type: z.literal('base64'),
|
||||
media_type: z.string().min(1),
|
||||
data: z.string().min(1),
|
||||
})
|
||||
.optional(),
|
||||
filename: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const mothershipExecuteBodySchema = z.object({
|
||||
messages: z.array(mothershipExecuteMessageSchema).min(1, 'At least one message is required'),
|
||||
responseFormat: z.any().optional(),
|
||||
workspaceId: z.string().min(1, 'workspaceId is required'),
|
||||
userId: z.string().min(1, 'userId is required'),
|
||||
chatId: z.string().optional(),
|
||||
messageId: z.string().optional(),
|
||||
requestId: z.string().optional(),
|
||||
fileAttachments: z.array(mothershipExecuteFileAttachmentSchema).optional(),
|
||||
/**
|
||||
* `@`-mentioned resources / `/`-invoked skills to resolve into the agent run,
|
||||
* mirroring the interactive chat path. Used by scheduled tasks, whose
|
||||
* captured contexts must reach the run without a live client.
|
||||
*/
|
||||
contexts: z.array(scheduleContextSchema).optional(),
|
||||
workflowId: z.string().optional(),
|
||||
executionId: z.string().optional(),
|
||||
userMetadata: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
timezone: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
export type MothershipExecuteBody = z.input<typeof mothershipExecuteBodySchema>
|
||||
|
||||
export const mothershipEventsQuerySchema = z
|
||||
.object({
|
||||
workspaceId: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const mothershipChatGetQuerySchema = z
|
||||
.object({
|
||||
workflowId: z.string().optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
chatId: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const mothershipChatPostEnvelopeSchema = z
|
||||
.object({
|
||||
message: z.string().optional(),
|
||||
chatId: z.string().optional(),
|
||||
workflowId: z.string().optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const mothershipChatStopEnvelopeSchema = z
|
||||
.object({
|
||||
chatId: z.string().optional(),
|
||||
streamId: z.string().optional(),
|
||||
content: z.string().optional(),
|
||||
contentBlocks: z.array(z.unknown()).optional(),
|
||||
requestId: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const mothershipChatAbortEnvelopeSchema = z
|
||||
.object({
|
||||
streamId: z.string().optional(),
|
||||
chatId: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const mothershipChatStreamQuerySchema = z
|
||||
.object({
|
||||
streamId: z.string().optional(),
|
||||
after: z.string().optional(),
|
||||
batch: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const mothershipChatResourceEnvelopeSchema = z
|
||||
.object({ chatId: z.string().optional() })
|
||||
.passthrough()
|
||||
|
||||
export const adminMothershipQuerySchema = z
|
||||
.object({
|
||||
env: z.preprocess(
|
||||
(value) => (value === '' || value === undefined ? 'dev' : value),
|
||||
z.string().min(1).default('dev')
|
||||
),
|
||||
endpoint: z.string().min(1, 'endpoint query param required'),
|
||||
})
|
||||
.passthrough()
|
||||
export type AdminMothershipQuery = z.output<typeof adminMothershipQuerySchema>
|
||||
|
||||
const mothershipChatResourceItemSchema = z.object({
|
||||
type: z.string(),
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
})
|
||||
|
||||
const mothershipChatResourcesResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
resources: z.array(mothershipChatResourceItemSchema),
|
||||
})
|
||||
|
||||
const addMothershipChatResourceBodySchema = z.object({
|
||||
chatId: z.string().min(1),
|
||||
resource: mothershipChatResourceItemSchema,
|
||||
})
|
||||
|
||||
const reorderMothershipChatResourcesBodySchema = z.object({
|
||||
chatId: z.string().min(1),
|
||||
resources: z.array(mothershipChatResourceItemSchema),
|
||||
})
|
||||
|
||||
const removeMothershipChatResourceBodySchema = z.object({
|
||||
chatId: z.string().min(1),
|
||||
resourceType: z.string().min(1),
|
||||
resourceId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const addMothershipChatResourceContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/mothership/chat/resources',
|
||||
body: addMothershipChatResourceBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: mothershipChatResourcesResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const reorderMothershipChatResourcesContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/mothership/chat/resources',
|
||||
body: reorderMothershipChatResourcesBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: mothershipChatResourcesResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const removeMothershipChatResourceContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/mothership/chat/resources',
|
||||
body: removeMothershipChatResourceBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: mothershipChatResourcesResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const mothershipChatSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string().nullable(),
|
||||
updatedAt: dateStringSchema,
|
||||
activeStreamId: z.string().nullable(),
|
||||
lastSeenAt: dateStringSchema.nullable(),
|
||||
pinned: z.boolean(),
|
||||
})
|
||||
|
||||
export const listMothershipChatsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/mothership/chats',
|
||||
query: listMothershipChatsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
data: z.array(mothershipChatSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateMothershipChatContract = defineRouteContract({
|
||||
method: 'PATCH',
|
||||
path: '/api/mothership/chats/[chatId]',
|
||||
params: mothershipChatParamsSchema,
|
||||
body: updateMothershipChatBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteMothershipChatContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/mothership/chats/[chatId]',
|
||||
params: mothershipChatParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const forkMothershipChatBodySchema = z.object({
|
||||
upToMessageId: z.string().min(1, 'upToMessageId is required'),
|
||||
})
|
||||
export type ForkMothershipChatBody = z.input<typeof forkMothershipChatBodySchema>
|
||||
|
||||
export const forkMothershipChatContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/mothership/chats/[chatId]/fork',
|
||||
params: mothershipChatParamsSchema,
|
||||
body: forkMothershipChatBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
id: z.string(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const createMothershipChatResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
export const mothershipExecuteResponseSchema = z
|
||||
.object({
|
||||
content: z.string().optional(),
|
||||
model: z.literal('mothership'),
|
||||
conversationId: z.string(),
|
||||
tokens: z
|
||||
.object({
|
||||
prompt: z.number().optional(),
|
||||
completion: z.number().optional(),
|
||||
total: z.number().optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
cost: z.unknown().optional(),
|
||||
toolCalls: z.array(z.unknown()).optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const mothershipChatStreamSnapshotSchema = z
|
||||
.object({
|
||||
events: z.array(z.unknown()),
|
||||
previewSessions: z.array(z.unknown()),
|
||||
status: z.string(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const getMothershipChatResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
chat: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
title: z.string().nullable(),
|
||||
messages: z.array(z.unknown()),
|
||||
activeStreamId: z.string().nullable(),
|
||||
resources: z.array(z.unknown()),
|
||||
createdAt: z.union([z.string(), z.date()]).nullable().optional(),
|
||||
updatedAt: z.union([z.string(), z.date()]).nullable().optional(),
|
||||
streamSnapshot: mothershipChatStreamSnapshotSchema.optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
})
|
||||
|
||||
export const createMothershipChatContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/mothership/chats',
|
||||
body: createMothershipChatBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: createMothershipChatResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const mothershipExecuteContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/mothership/execute',
|
||||
body: mothershipExecuteBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: mothershipExecuteResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getMothershipChatContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/mothership/chats/[chatId]',
|
||||
params: mothershipChatParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: getMothershipChatResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export type MothershipChat = z.infer<typeof mothershipChatSchema>
|
||||
@@ -0,0 +1,209 @@
|
||||
import { z } from 'zod'
|
||||
import { workspaceIdSchema } from '@/lib/api/contracts/primitives'
|
||||
import type {
|
||||
ContractBody,
|
||||
ContractBodyInput,
|
||||
ContractJsonResponse,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const oauthAccountSummarySchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
export type OAuthAccountSummary = z.output<typeof oauthAccountSummarySchema>
|
||||
|
||||
export const oauthConnectionSchema = z.object({
|
||||
provider: z.string(),
|
||||
baseProvider: z.string(),
|
||||
featureType: z.string(),
|
||||
isConnected: z.boolean(),
|
||||
accounts: z.array(oauthAccountSummarySchema),
|
||||
lastConnected: z.string(),
|
||||
scopes: z.array(z.string()),
|
||||
})
|
||||
export type OAuthConnection = z.output<typeof oauthConnectionSchema>
|
||||
|
||||
export const disconnectOAuthBodySchema = z.object({
|
||||
provider: z.string({ error: 'Provider is required' }).min(1, 'Provider is required'),
|
||||
providerId: z.string().optional(),
|
||||
accountId: z.string().optional(),
|
||||
})
|
||||
|
||||
export const connectedAccountsQuerySchema = z.object({
|
||||
provider: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
export const connectedAccountSchema = z.object({
|
||||
id: z.string(),
|
||||
accountId: z.string(),
|
||||
providerId: z.string(),
|
||||
displayName: z.string(),
|
||||
})
|
||||
export type ConnectedAccount = z.output<typeof connectedAccountSchema>
|
||||
|
||||
export const trelloTokenBodySchema = z.object({
|
||||
token: z.string().min(1),
|
||||
state: z.string().min(1, 'state is required'),
|
||||
})
|
||||
|
||||
const emptyTrelloAuthQuerySchema = z.object({}).passthrough()
|
||||
|
||||
const trelloCallbackQuerySchema = z
|
||||
.object({
|
||||
state: z.string().min(1).optional(),
|
||||
error: z.string().min(1).optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const oauthTokenRequestBodySchema = z
|
||||
.object({
|
||||
credentialId: z.string().min(1).optional(),
|
||||
credentialAccountUserId: z.string().min(1).optional(),
|
||||
providerId: z.string().min(1).optional(),
|
||||
workflowId: z.string().min(1).nullish(),
|
||||
scopes: z.array(z.string()).optional(),
|
||||
impersonateEmail: z.string().email().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => data.credentialId || (data.credentialAccountUserId && data.providerId),
|
||||
'Either credentialId or (credentialAccountUserId + providerId) is required'
|
||||
)
|
||||
|
||||
export const oauthTokenGetQuerySchema = z.object({
|
||||
credentialId: z
|
||||
.string({
|
||||
error: 'Credential ID is required',
|
||||
})
|
||||
.min(1, 'Credential ID is required'),
|
||||
})
|
||||
|
||||
export const oauthTokenPostQuerySchema = z.object({
|
||||
userId: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
const oauthTokenResponseSchema = z.object({
|
||||
accessToken: z.string(),
|
||||
idToken: z.string().optional(),
|
||||
instanceUrl: z.string().optional(),
|
||||
cloudId: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
})
|
||||
|
||||
export const oauthTokenGetContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/auth/oauth/token',
|
||||
query: oauthTokenGetQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: oauthTokenResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const oauthTokenPostContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/auth/oauth/token',
|
||||
query: oauthTokenPostQuerySchema,
|
||||
body: oauthTokenRequestBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: oauthTokenResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const shopifyAuthorizeQuerySchema = z.object({
|
||||
shop: z.string().optional(),
|
||||
returnUrl: z.string().optional(),
|
||||
})
|
||||
|
||||
export const shopifyCallbackQuerySchema = z.object({
|
||||
code: z.string().optional(),
|
||||
state: z.string().optional(),
|
||||
shop: z.string().optional(),
|
||||
})
|
||||
|
||||
export const shopifyStoreCookieSchema = z.object({
|
||||
accessToken: z.string().min(1),
|
||||
shopDomain: z.string().min(1),
|
||||
scope: z.string().optional(),
|
||||
returnUrl: z.string().optional(),
|
||||
})
|
||||
|
||||
const SHOPIFY_SHOP_DOMAIN_REGEX = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]\.myshopify\.com$/
|
||||
export const shopifyShopDomainSchema = z.string().regex(SHOPIFY_SHOP_DOMAIN_REGEX)
|
||||
|
||||
export const listOAuthConnectionsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/auth/oauth/connections',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
connections: z.array(oauthConnectionSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const disconnectOAuthContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/auth/oauth/disconnect',
|
||||
body: disconnectOAuthBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const listConnectedAccountsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/auth/accounts',
|
||||
query: connectedAccountsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
accounts: z.array(connectedAccountSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const storeTrelloTokenContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/auth/trello/store',
|
||||
body: trelloTokenBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({ success: z.boolean(), error: z.string().optional() }),
|
||||
},
|
||||
})
|
||||
|
||||
export const authorizeTrelloContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/auth/trello/authorize',
|
||||
query: emptyTrelloAuthQuerySchema,
|
||||
response: { mode: 'redirect' },
|
||||
})
|
||||
|
||||
export const trelloCallbackContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/auth/trello/callback',
|
||||
query: trelloCallbackQuerySchema,
|
||||
response: { mode: 'text' },
|
||||
})
|
||||
|
||||
export const authorizeOAuth2QuerySchema = z.object({
|
||||
providerId: z.string().min(1, 'providerId is required'),
|
||||
workspaceId: workspaceIdSchema,
|
||||
callbackURL: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
export const authorizeOAuth2Contract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/auth/oauth2/authorize',
|
||||
query: authorizeOAuth2QuerySchema,
|
||||
response: { mode: 'redirect' },
|
||||
})
|
||||
|
||||
export type StoreTrelloTokenBody = ContractBody<typeof storeTrelloTokenContract>
|
||||
export type StoreTrelloTokenBodyInput = ContractBodyInput<typeof storeTrelloTokenContract>
|
||||
export type StoreTrelloTokenResponse = ContractJsonResponse<typeof storeTrelloTokenContract>
|
||||
@@ -0,0 +1,614 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
type PiiRedactionSettings,
|
||||
piiRedactionSettingsSchema,
|
||||
retentionOverridesSchema,
|
||||
workspaceIdSchema,
|
||||
} from '@/lib/api/contracts/primitives'
|
||||
import { organizationBillingDataSchema } from '@/lib/api/contracts/subscription'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import { workspacePermissionSchema } from '@/lib/api/contracts/workspaces'
|
||||
import { HEX_COLOR_REGEX } from '@/lib/branding'
|
||||
|
||||
const booleanQueryParamSchema = z
|
||||
.preprocess((value) => {
|
||||
if (value === 'true') return true
|
||||
if (value === 'false') return false
|
||||
return value
|
||||
}, z.boolean())
|
||||
.optional()
|
||||
|
||||
const numericResponseSchema = z.preprocess((value) => {
|
||||
if (typeof value !== 'string') return value
|
||||
const parsed = Number.parseFloat(value)
|
||||
return Number.isFinite(parsed) ? parsed : value
|
||||
}, z.number())
|
||||
|
||||
export const organizationRoleSchema = z.enum(['owner', 'admin', 'member'], {
|
||||
error: 'Invalid role',
|
||||
})
|
||||
export const organizationParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const organizationMemberParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
memberId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const organizationMemberQuerySchema = z
|
||||
.object({
|
||||
include: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const workspaceGrantSchema = z.object({
|
||||
workspaceId: z.string().min(1),
|
||||
permission: workspacePermissionSchema,
|
||||
})
|
||||
|
||||
export const createOrganizationBodySchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
slug: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const updateOrganizationBodySchema = z.object({
|
||||
name: z.string().trim().min(1, 'Organization name is required').optional(),
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Organization slug is required')
|
||||
.regex(
|
||||
/^[a-z0-9-_]+$/,
|
||||
'Slug can only contain lowercase letters, numbers, hyphens, and underscores'
|
||||
)
|
||||
.optional(),
|
||||
logo: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
export const createOrganizationInvitationBodySchema = z
|
||||
.object({
|
||||
email: z.string().optional(),
|
||||
emails: z.array(z.string()).optional(),
|
||||
role: z.enum(['member', 'admin'], { error: 'Invalid role' }).optional(),
|
||||
workspaceInvitations: z.array(workspaceGrantSchema).optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const organizationInvitationsQuerySchema = z
|
||||
.object({
|
||||
validate: booleanQueryParamSchema,
|
||||
batch: booleanQueryParamSchema,
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const updateOrganizationMemberRoleBodySchema = z.object({
|
||||
role: organizationRoleSchema,
|
||||
})
|
||||
|
||||
export const inviteOrganizationMemberBodySchema = z
|
||||
.object({
|
||||
email: z.string({ error: 'Email is required' }).min(1, 'Email is required'),
|
||||
role: z.enum(['admin', 'member'], { error: 'Invalid role' }).optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const organizationDataRetentionHoursSchema = z
|
||||
.number()
|
||||
.int()
|
||||
.min(24)
|
||||
.max(43800)
|
||||
.nullable()
|
||||
.optional()
|
||||
|
||||
export type { PiiRedactionSettings }
|
||||
|
||||
export const updateOrganizationDataRetentionBodySchema = z.object({
|
||||
logRetentionHours: organizationDataRetentionHoursSchema,
|
||||
softDeleteRetentionHours: organizationDataRetentionHoursSchema,
|
||||
taskCleanupHours: organizationDataRetentionHoursSchema,
|
||||
piiRedaction: piiRedactionSettingsSchema.optional(),
|
||||
retentionOverrides: retentionOverridesSchema.optional(),
|
||||
})
|
||||
|
||||
export type UpdateOrganizationDataRetentionBody = z.input<
|
||||
typeof updateOrganizationDataRetentionBodySchema
|
||||
>
|
||||
|
||||
const organizationRetentionValuesSchema = z.object({
|
||||
logRetentionHours: z.number().int().nullable(),
|
||||
softDeleteRetentionHours: z.number().int().nullable(),
|
||||
taskCleanupHours: z.number().int().nullable(),
|
||||
piiRedaction: piiRedactionSettingsSchema.nullable(),
|
||||
retentionOverrides: retentionOverridesSchema.nullable(),
|
||||
})
|
||||
|
||||
export type OrganizationRetentionValues = z.output<typeof organizationRetentionValuesSchema>
|
||||
|
||||
const organizationDataRetentionDataSchema = z.object({
|
||||
isEnterprise: z.boolean(),
|
||||
defaults: organizationRetentionValuesSchema,
|
||||
configured: organizationRetentionValuesSchema,
|
||||
effective: organizationRetentionValuesSchema,
|
||||
piiRedactionEnabled: z.boolean(),
|
||||
piiGranularRedactionEnabled: z.boolean(),
|
||||
})
|
||||
|
||||
export type OrganizationDataRetention = z.output<typeof organizationDataRetentionDataSchema>
|
||||
|
||||
export const organizationDataRetentionResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
data: organizationDataRetentionDataSchema,
|
||||
})
|
||||
|
||||
export const updateOrganizationWhitelabelBodySchema = z.object({
|
||||
brandName: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(64, 'Brand name must be 64 characters or fewer')
|
||||
.nullable()
|
||||
.optional(),
|
||||
logoUrl: z.string().min(1).nullable().optional(),
|
||||
wordmarkUrl: z.string().min(1).nullable().optional(),
|
||||
primaryColor: z
|
||||
.string()
|
||||
.regex(HEX_COLOR_REGEX, 'Primary color must be a valid hex color (e.g. #33c482)')
|
||||
.nullable()
|
||||
.optional(),
|
||||
primaryHoverColor: z
|
||||
.string()
|
||||
.regex(HEX_COLOR_REGEX, 'Primary hover color must be a valid hex color')
|
||||
.nullable()
|
||||
.optional(),
|
||||
accentColor: z
|
||||
.string()
|
||||
.regex(HEX_COLOR_REGEX, 'Accent color must be a valid hex color')
|
||||
.nullable()
|
||||
.optional(),
|
||||
accentHoverColor: z
|
||||
.string()
|
||||
.regex(HEX_COLOR_REGEX, 'Accent hover color must be a valid hex color')
|
||||
.nullable()
|
||||
.optional(),
|
||||
supportEmail: z
|
||||
.string()
|
||||
.email('Support email must be a valid email address')
|
||||
.nullable()
|
||||
.optional(),
|
||||
documentationUrl: z.string().url('Documentation URL must be a valid URL').nullable().optional(),
|
||||
termsUrl: z.string().url('Terms URL must be a valid URL').nullable().optional(),
|
||||
privacyUrl: z.string().url('Privacy URL must be a valid URL').nullable().optional(),
|
||||
hidePoweredBySim: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const transferOwnershipBodySchema = z.object({
|
||||
newOwnerUserId: z.string().min(1),
|
||||
alsoLeave: z.boolean().optional().default(false),
|
||||
})
|
||||
|
||||
export const rosterWorkspaceAccessSchema = z.object({
|
||||
workspaceId: z.string(),
|
||||
workspaceName: z.string(),
|
||||
permission: workspacePermissionSchema,
|
||||
})
|
||||
|
||||
export const rosterMemberSchema = z.object({
|
||||
memberId: z.string(),
|
||||
userId: z.string(),
|
||||
role: z.enum(['owner', 'admin', 'member', 'external']),
|
||||
createdAt: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
image: z.string().nullable(),
|
||||
workspaces: z.array(rosterWorkspaceAccessSchema),
|
||||
})
|
||||
|
||||
export const rosterPendingInvitationSchema = z.object({
|
||||
id: z.string(),
|
||||
email: z.string(),
|
||||
role: z.string(),
|
||||
kind: z.enum(['organization', 'workspace']),
|
||||
membershipIntent: z.enum(['internal', 'external']).optional(),
|
||||
createdAt: z.string(),
|
||||
expiresAt: z.string(),
|
||||
inviteeName: z.string().nullable(),
|
||||
inviteeImage: z.string().nullable(),
|
||||
workspaces: z.array(rosterWorkspaceAccessSchema),
|
||||
})
|
||||
|
||||
export const organizationRosterSchema = z.object({
|
||||
members: z.array(rosterMemberSchema),
|
||||
pendingInvitations: z.array(rosterPendingInvitationSchema),
|
||||
workspaces: z.array(z.object({ id: z.string(), name: z.string() })),
|
||||
})
|
||||
|
||||
export const organizationMemberUsageSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
userId: z.string(),
|
||||
organizationId: z.string(),
|
||||
role: organizationRoleSchema,
|
||||
createdAt: z.string(),
|
||||
userName: z.string().nullable(),
|
||||
userEmail: z.string().nullable(),
|
||||
currentPeriodCost: numericResponseSchema.nullable().optional(),
|
||||
currentUsageLimit: numericResponseSchema.nullable().optional(),
|
||||
usageLimitUpdatedAt: z.string().nullable().optional(),
|
||||
billingPeriodStart: z.string().nullable().optional(),
|
||||
billingPeriodEnd: z.string().nullable().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const listOrganizationMembersResponseSchema = z
|
||||
.object({
|
||||
success: z.boolean(),
|
||||
data: z.array(organizationMemberUsageSchema),
|
||||
total: z.number(),
|
||||
userRole: organizationRoleSchema,
|
||||
hasAdminAccess: z.boolean(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const successResponseSchema = z
|
||||
.object({
|
||||
success: z.boolean(),
|
||||
message: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const organizationInvitationValidationResponseSchema = z
|
||||
.object({
|
||||
success: z.literal(true),
|
||||
data: z.unknown(),
|
||||
validatedBy: z.string(),
|
||||
validatedAt: z.string(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const getOrganizationRosterContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/organizations/[id]/roster',
|
||||
params: organizationParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.boolean(),
|
||||
data: organizationRosterSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const listOrganizationMembersContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/organizations/[id]/members',
|
||||
params: organizationParamsSchema,
|
||||
query: organizationMemberQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: listOrganizationMembersResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const inviteOrganizationMemberContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/organizations/[id]/members',
|
||||
params: organizationParamsSchema,
|
||||
body: inviteOrganizationMemberBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema.extend({
|
||||
data: z
|
||||
.object({
|
||||
invitationId: z.string(),
|
||||
email: z.string(),
|
||||
role: organizationRoleSchema,
|
||||
})
|
||||
.passthrough()
|
||||
.optional(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const inviteOrganizationMembersContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/organizations/[id]/invitations',
|
||||
params: organizationParamsSchema,
|
||||
query: organizationInvitationsQuerySchema,
|
||||
body: createOrganizationInvitationBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.union([
|
||||
organizationInvitationValidationResponseSchema,
|
||||
successResponseSchema.extend({
|
||||
error: z.string().optional(),
|
||||
data: z
|
||||
.object({
|
||||
invitationsSent: z.number(),
|
||||
invitedEmails: z.array(z.string()),
|
||||
directlyAdded: z.array(z.string()).optional(),
|
||||
directlyAddedCount: z.number().optional(),
|
||||
failedInvitations: z.array(z.object({ email: z.string(), error: z.string() })),
|
||||
existingMembers: z.array(z.string()),
|
||||
pendingInvitations: z.array(z.string()),
|
||||
invalidEmails: z.array(z.string()),
|
||||
workspaceGrantsPerInvite: z.number(),
|
||||
seatInfo: z
|
||||
.object({
|
||||
seatsUsed: z.number(),
|
||||
maxSeats: z.number(),
|
||||
availableSeats: z.number(),
|
||||
})
|
||||
.passthrough()
|
||||
.optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.optional(),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateOrganizationMemberRoleContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/organizations/[id]/members/[memberId]',
|
||||
params: organizationMemberParamsSchema,
|
||||
body: updateOrganizationMemberRoleBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema.extend({
|
||||
data: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
userId: z.string(),
|
||||
role: organizationRoleSchema,
|
||||
updatedBy: z.string(),
|
||||
})
|
||||
.passthrough()
|
||||
.optional(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const removeOrganizationMemberContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/organizations/[id]/members/[memberId]',
|
||||
params: organizationMemberParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema.extend({
|
||||
data: z.record(z.string(), z.unknown()).optional(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
/** Per-member credit usage + cap for the Manage Credits modal (values in credits). */
|
||||
export const organizationMemberUsageLimitDataSchema = z.object({
|
||||
creditsUsed: z.number(),
|
||||
creditLimit: z.number().nullable(),
|
||||
/** Billing cadence of the org's subscription, so the UI can label the usage window. */
|
||||
billingInterval: z.enum(['month', 'year']),
|
||||
})
|
||||
|
||||
export const getOrganizationMemberUsageLimitContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/organizations/[id]/members/[memberId]/usage-limit',
|
||||
params: organizationMemberParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.boolean(),
|
||||
data: organizationMemberUsageLimitDataSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateOrganizationMemberUsageLimitBodySchema = z.object({
|
||||
/** New cap in credits; `null` clears the per-member cap. */
|
||||
creditLimit: z
|
||||
.number()
|
||||
.int('Credit limit must be a whole number of credits')
|
||||
.min(0, 'Credit limit cannot be negative')
|
||||
.nullable(),
|
||||
})
|
||||
|
||||
export const updateOrganizationMemberUsageLimitContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/organizations/[id]/members/[memberId]/usage-limit',
|
||||
params: organizationMemberParamsSchema,
|
||||
body: updateOrganizationMemberUsageLimitBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema.extend({
|
||||
data: z
|
||||
.object({
|
||||
creditLimit: z.number().nullable(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Self-service per-member usage for the chat-home credits chip. Values are in
|
||||
* DOLLARS (the DB unit) so the client's `formatCredits` performs the single
|
||||
* dollars→credits conversion — returning credits here would double-convert.
|
||||
* `limitDollars` is null when no per-member cap applies (non-hosted, the
|
||||
* workspace isn't org-owned, or no cap is set), so the chip falls back to the
|
||||
* plan-level credits view.
|
||||
*/
|
||||
export const myMemberCreditsDataSchema = z.object({
|
||||
usedDollars: z.number(),
|
||||
limitDollars: z.number().nullable(),
|
||||
})
|
||||
export type MyMemberCreditsData = z.infer<typeof myMemberCreditsDataSchema>
|
||||
|
||||
/**
|
||||
* Own-data-only (no admin gate, unlike the admin route above) and workspace-
|
||||
* scoped, so the chat-home chip can resolve the acting member's own remaining.
|
||||
*/
|
||||
export const getMyMemberCreditsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/billing/member-credits',
|
||||
query: z.object({ workspaceId: workspaceIdSchema }),
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.boolean(),
|
||||
data: myMemberCreditsDataSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const transferOwnershipContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/organizations/[id]/transfer-ownership',
|
||||
params: organizationParamsSchema,
|
||||
body: transferOwnershipBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z
|
||||
.object({
|
||||
success: z.boolean(),
|
||||
transferred: z.boolean(),
|
||||
left: z.boolean(),
|
||||
warning: z.string().optional(),
|
||||
details: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateOrganizationContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/organizations/[id]',
|
||||
params: organizationParamsSchema,
|
||||
body: updateOrganizationBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema.extend({
|
||||
data: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string().nullable(),
|
||||
logo: z.string().nullable(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
.passthrough()
|
||||
.optional(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const getOrganizationDataRetentionContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/organizations/[id]/data-retention',
|
||||
params: organizationParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: organizationDataRetentionResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateOrganizationDataRetentionContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/organizations/[id]/data-retention',
|
||||
params: organizationParamsSchema,
|
||||
body: updateOrganizationDataRetentionBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: organizationDataRetentionResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
// Read shape mirrors `OrganizationWhitelabelSettings` from
|
||||
// `@/lib/branding/types`. All fields are optional (nullable on the way in
|
||||
// for the PUT contract, but stored without nulls on the way out — the
|
||||
// route deletes keys that are explicitly cleared).
|
||||
export const organizationWhitelabelSettingsResponseSchema = z.object({
|
||||
brandName: z.string().optional(),
|
||||
logoUrl: z.string().optional(),
|
||||
wordmarkUrl: z.string().optional(),
|
||||
primaryColor: z.string().optional(),
|
||||
primaryHoverColor: z.string().optional(),
|
||||
accentColor: z.string().optional(),
|
||||
accentHoverColor: z.string().optional(),
|
||||
supportEmail: z.string().optional(),
|
||||
documentationUrl: z.string().optional(),
|
||||
termsUrl: z.string().optional(),
|
||||
privacyUrl: z.string().optional(),
|
||||
hidePoweredBySim: z.boolean().optional(),
|
||||
})
|
||||
|
||||
const organizationWhitelabelEnvelopeResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
data: organizationWhitelabelSettingsResponseSchema,
|
||||
})
|
||||
|
||||
export const getOrganizationWhitelabelContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/organizations/[id]/whitelabel',
|
||||
params: organizationParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: organizationWhitelabelEnvelopeResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateOrganizationWhitelabelContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/organizations/[id]/whitelabel',
|
||||
params: organizationParamsSchema,
|
||||
body: updateOrganizationWhitelabelBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: organizationWhitelabelEnvelopeResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const createOrganizationContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/organizations',
|
||||
body: createOrganizationBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.boolean(),
|
||||
organizationId: z.string(),
|
||||
created: z.boolean(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const updateOrganizationUsageLimitContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/usage',
|
||||
body: z.object({
|
||||
context: z.literal('organization'),
|
||||
organizationId: z.string().min(1),
|
||||
limit: z.number().min(0, 'Limit must be a non-negative number'),
|
||||
}),
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z
|
||||
.object({
|
||||
success: z.boolean(),
|
||||
context: z.literal('organization'),
|
||||
userId: z.string(),
|
||||
organizationId: z.string(),
|
||||
data: organizationBillingDataSchema.nullable(),
|
||||
})
|
||||
.passthrough(),
|
||||
},
|
||||
})
|
||||
|
||||
export type OrganizationRoster = z.infer<typeof organizationRosterSchema>
|
||||
export type RosterWorkspaceAccess = z.infer<typeof rosterWorkspaceAccessSchema>
|
||||
export type RosterMember = z.infer<typeof rosterMemberSchema>
|
||||
export type RosterPendingInvitation = z.infer<typeof rosterPendingInvitationSchema>
|
||||
export type OrganizationMembersResponse = z.infer<typeof listOrganizationMembersResponseSchema>
|
||||
export type OrganizationMemberUsageLimitData = z.infer<
|
||||
typeof organizationMemberUsageLimitDataSchema
|
||||
>
|
||||
@@ -0,0 +1,27 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
/**
|
||||
* Boundary contract for listing the organizations the current user belongs to.
|
||||
* Backs `GET /api/organizations`.
|
||||
*/
|
||||
|
||||
export const creatorOrganizationSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
role: z.string(),
|
||||
})
|
||||
|
||||
export type CreatorOrganization = z.output<typeof creatorOrganizationSchema>
|
||||
|
||||
export const listCreatorOrganizationsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/organizations',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
organizations: z.array(creatorOrganizationSchema),
|
||||
isMemberOfAnyOrg: z.boolean(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
createPermissionGroupBodySchema,
|
||||
updatePermissionGroupBodySchema,
|
||||
} from '@/lib/api/contracts/permission-groups'
|
||||
|
||||
describe('createPermissionGroupBodySchema', () => {
|
||||
it('accepts a name-only body (scope is resolved and validated server-side)', () => {
|
||||
const result = createPermissionGroupBodySchema.safeParse({ name: 'Engineering' })
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts a default group', () => {
|
||||
const result = createPermissionGroupBodySchema.safeParse({
|
||||
name: 'Baseline',
|
||||
isDefault: true,
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts a specific-scope group with workspaces', () => {
|
||||
const result = createPermissionGroupBodySchema.safeParse({
|
||||
name: 'Contractors',
|
||||
workspaceIds: ['ws-1'],
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts a name + empty workspaces (the create route enforces at least one)', () => {
|
||||
const result = createPermissionGroupBodySchema.safeParse({
|
||||
name: 'Contractors',
|
||||
workspaceIds: [],
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects a default group that targets specific workspaces', () => {
|
||||
const result = createPermissionGroupBodySchema.safeParse({
|
||||
name: 'Baseline',
|
||||
isDefault: true,
|
||||
workspaceIds: ['ws-1'],
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updatePermissionGroupBodySchema', () => {
|
||||
it('accepts an empty update', () => {
|
||||
expect(updatePermissionGroupBodySchema.safeParse({}).success).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts demoting the default via isDefault:false alone (the route re-scopes it)', () => {
|
||||
expect(updatePermissionGroupBodySchema.safeParse({ isDefault: false }).success).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts emptying scope (group then governs nothing)', () => {
|
||||
const result = updatePermissionGroupBodySchema.safeParse({ workspaceIds: [] })
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts a specific scope with workspaces', () => {
|
||||
const result = updatePermissionGroupBodySchema.safeParse({
|
||||
workspaceIds: ['ws-1', 'ws-2'],
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts promoting a group to the default', () => {
|
||||
expect(updatePermissionGroupBodySchema.safeParse({ isDefault: true }).success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects promoting to the default while naming workspaces', () => {
|
||||
const result = updatePermissionGroupBodySchema.safeParse({
|
||||
isDefault: true,
|
||||
workspaceIds: ['ws-1'],
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,313 @@
|
||||
import { z } from 'zod'
|
||||
import { organizationIdSchema } from '@/lib/api/contracts/primitives'
|
||||
import { shareAuthTypeSchema } from '@/lib/api/contracts/public-shares'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import { permissionGroupConfigSchema } from '@/lib/permission-groups/types'
|
||||
|
||||
export const permissionGroupFullConfigSchema = z.object({
|
||||
allowedIntegrations: z.array(z.string()).nullable(),
|
||||
allowedModelProviders: z.array(z.string()).nullable(),
|
||||
deniedModels: z.array(z.string()).default([]),
|
||||
deniedTools: z.array(z.string()).default([]),
|
||||
hideTraceSpans: z.boolean(),
|
||||
hideKnowledgeBaseTab: z.boolean(),
|
||||
hideTablesTab: z.boolean(),
|
||||
hideCopilot: z.boolean(),
|
||||
hideIntegrationsTab: z.boolean(),
|
||||
hideSecretsTab: z.boolean(),
|
||||
hideApiKeysTab: z.boolean(),
|
||||
hideInboxTab: z.boolean(),
|
||||
hideFilesTab: z.boolean(),
|
||||
disableMcpTools: z.boolean(),
|
||||
disableCustomTools: z.boolean(),
|
||||
disableSkills: z.boolean(),
|
||||
disableInvitations: z.boolean(),
|
||||
disablePublicApi: z.boolean(),
|
||||
disablePublicFileSharing: z.boolean(),
|
||||
allowedFileShareAuthTypes: z.array(shareAuthTypeSchema).nullable(),
|
||||
hideDeployApi: z.boolean(),
|
||||
hideDeployMcp: z.boolean(),
|
||||
hideDeployChatbot: z.boolean(),
|
||||
hideDeployTemplate: z.boolean(),
|
||||
})
|
||||
|
||||
export const addPermissionGroupMemberBodySchema = z.object({
|
||||
userId: z.string().min(1),
|
||||
})
|
||||
|
||||
/** Route params for organization-scoped permission-group collection routes (`id` = organizationId). */
|
||||
export const permissionGroupParamsSchema = z.object({
|
||||
id: organizationIdSchema,
|
||||
})
|
||||
|
||||
/** Route params for a single permission group (`id` = organizationId, `groupId` = permission group id). */
|
||||
export const permissionGroupDetailParamsSchema = z.object({
|
||||
id: organizationIdSchema,
|
||||
groupId: z.string().min(1),
|
||||
})
|
||||
|
||||
/** A workspace a permission group targets (id + display name). */
|
||||
export const permissionGroupWorkspaceRefSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
export type PermissionGroupWorkspaceRef = z.output<typeof permissionGroupWorkspaceRefSchema>
|
||||
|
||||
export const permissionGroupSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
config: permissionGroupFullConfigSchema,
|
||||
createdBy: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
creatorName: z.string().nullable(),
|
||||
creatorEmail: z.string().nullable(),
|
||||
memberCount: z.number(),
|
||||
isDefault: z.boolean(),
|
||||
/**
|
||||
* Workspaces this group targets. Empty for the default group (which governs
|
||||
* every workspace) and for a non-default group scoped to nothing.
|
||||
*/
|
||||
workspaces: z.array(permissionGroupWorkspaceRefSchema),
|
||||
})
|
||||
export type PermissionGroup = z.output<typeof permissionGroupSchema>
|
||||
|
||||
export const permissionGroupWriteSchema = z.object({
|
||||
id: z.string(),
|
||||
organizationId: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
config: permissionGroupFullConfigSchema,
|
||||
createdBy: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
isDefault: z.boolean(),
|
||||
/** Ids of targeted workspaces (empty for the default group). */
|
||||
workspaceIds: z.array(z.string()),
|
||||
})
|
||||
export type PermissionGroupWrite = z.output<typeof permissionGroupWriteSchema>
|
||||
|
||||
export const permissionGroupMemberSchema = z.object({
|
||||
id: z.string(),
|
||||
userId: z.string(),
|
||||
assignedAt: z.string(),
|
||||
userName: z.string().nullable(),
|
||||
userEmail: z.string().nullable(),
|
||||
userImage: z.string().nullable(),
|
||||
})
|
||||
export type PermissionGroupMember = z.output<typeof permissionGroupMemberSchema>
|
||||
|
||||
export const userPermissionConfigQuerySchema = z.object({
|
||||
workspaceId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const userPermissionConfigSchema = z.object({
|
||||
permissionGroupId: z.string().nullable(),
|
||||
groupName: z.string().nullable(),
|
||||
config: permissionGroupFullConfigSchema.nullable(),
|
||||
entitled: z.boolean(),
|
||||
/** The workspace's owning organization id (null when the workspace has no org). */
|
||||
organizationId: z.string().nullable(),
|
||||
/** Whether the caller is an owner/admin of the workspace's owning organization. */
|
||||
isOrgAdmin: z.boolean(),
|
||||
})
|
||||
export type UserPermissionConfig = z.output<typeof userPermissionConfigSchema>
|
||||
|
||||
/** Upper bound on how many workspaces a single group can explicitly target. */
|
||||
export const MAX_PERMISSION_GROUP_WORKSPACES = 500
|
||||
|
||||
const workspaceIdsSchema = z.array(z.string().min(1)).max(MAX_PERMISSION_GROUP_WORKSPACES)
|
||||
|
||||
/**
|
||||
* The one cross-field scope rule shared by create and update: the organization
|
||||
* default group governs every workspace, so it cannot also name specific
|
||||
* workspaces (they would be silently dropped server-side). "Org-wide" is
|
||||
* definitionally `isDefault` — there is no separate flag — so a default group
|
||||
* with no `workspaceIds` is already the all-workspaces case and needs no
|
||||
* assertion here.
|
||||
*
|
||||
* Everything else is left to the routes: a non-default group targets the
|
||||
* workspaces in `workspaceIds` (empty is allowed on update — the group then
|
||||
* governs nothing, since the resolver inner-joins the workspace link table), and
|
||||
* the create route requires at least one workspace up front.
|
||||
*/
|
||||
function refineWorkspaceScope(
|
||||
body: { workspaceIds?: string[]; isDefault?: boolean },
|
||||
ctx: z.RefinementCtx
|
||||
) {
|
||||
if (body.isDefault === true && body.workspaceIds && body.workspaceIds.length > 0) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['workspaceIds'],
|
||||
message: 'The default group governs all workspaces and cannot target specific workspaces',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const createPermissionGroupBodySchema = z
|
||||
.object({
|
||||
name: z.string().trim().min(1).max(100),
|
||||
description: z.string().max(500).optional(),
|
||||
config: permissionGroupConfigSchema.optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
workspaceIds: workspaceIdsSchema.optional(),
|
||||
})
|
||||
.superRefine(refineWorkspaceScope)
|
||||
|
||||
export const updatePermissionGroupBodySchema = z
|
||||
.object({
|
||||
name: z.string().trim().min(1).max(100).optional(),
|
||||
description: z.string().max(500).nullable().optional(),
|
||||
config: permissionGroupConfigSchema.optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
workspaceIds: workspaceIdsSchema.optional(),
|
||||
})
|
||||
.superRefine(refineWorkspaceScope)
|
||||
|
||||
export const removePermissionGroupMemberQuerySchema = z.object({
|
||||
memberId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const bulkAddPermissionGroupMembersBodySchema = z.object({
|
||||
userIds: z.array(z.string()).optional(),
|
||||
addAllOrganizationMembers: z.boolean().optional(),
|
||||
})
|
||||
|
||||
const successResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
})
|
||||
|
||||
export const listPermissionGroupsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/organizations/[id]/permission-groups',
|
||||
params: permissionGroupParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
permissionGroups: z.array(permissionGroupSchema).optional(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const createPermissionGroupContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/organizations/[id]/permission-groups',
|
||||
params: permissionGroupParamsSchema,
|
||||
body: createPermissionGroupBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
permissionGroup: permissionGroupWriteSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const getUserPermissionConfigContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/permission-groups/user',
|
||||
query: userPermissionConfigQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: userPermissionConfigSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const updatePermissionGroupContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/organizations/[id]/permission-groups/[groupId]',
|
||||
params: permissionGroupDetailParamsSchema,
|
||||
body: updatePermissionGroupBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
permissionGroup: permissionGroupWriteSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const deletePermissionGroupContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/organizations/[id]/permission-groups/[groupId]',
|
||||
params: permissionGroupDetailParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const listPermissionGroupMembersContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/organizations/[id]/permission-groups/[groupId]/members',
|
||||
params: permissionGroupDetailParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
members: z.array(permissionGroupMemberSchema).optional(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const removePermissionGroupMemberContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/organizations/[id]/permission-groups/[groupId]/members',
|
||||
params: permissionGroupDetailParamsSchema,
|
||||
query: removePermissionGroupMemberQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const addPermissionGroupMemberContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/organizations/[id]/permission-groups/[groupId]/members',
|
||||
params: permissionGroupDetailParamsSchema,
|
||||
body: addPermissionGroupMemberBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
member: z.object({
|
||||
id: z.string(),
|
||||
permissionGroupId: z.string(),
|
||||
organizationId: z.string(),
|
||||
userId: z.string(),
|
||||
assignedBy: z.string(),
|
||||
assignedAt: z.string(),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const bulkAddPermissionGroupMembersContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/organizations/[id]/permission-groups/[groupId]/members/bulk',
|
||||
params: permissionGroupDetailParamsSchema,
|
||||
body: bulkAddPermissionGroupMembersBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
added: z.number(),
|
||||
// Users not added because they were already in this group. A conflicting
|
||||
// selection fails the whole request (409) rather than being skipped, so
|
||||
// the add is all-or-nothing for conflicts.
|
||||
skipped: z.number(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* List the workspaces belonging to an organization, used to populate the
|
||||
* workspace multi-select when scoping a permission group to specific workspaces.
|
||||
*/
|
||||
export const listOrganizationWorkspacesContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/organizations/[id]/workspaces',
|
||||
params: permissionGroupParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
workspaces: z.array(permissionGroupWorkspaceRefSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,238 @@
|
||||
import { z } from 'zod'
|
||||
import { PII_LANGUAGE_CODES } from '@/lib/guardrails/pii-entities'
|
||||
|
||||
export const unknownRecordSchema = z.record(z.string(), z.unknown())
|
||||
|
||||
export function flattenFieldErrors<TFields extends string>(
|
||||
error: z.ZodError
|
||||
): Partial<Record<TFields, string>> {
|
||||
const result: Partial<Record<TFields, string>> = {}
|
||||
for (const issue of error.issues) {
|
||||
const field = issue.path[0]
|
||||
if (typeof field !== 'string') continue
|
||||
if (result[field as TFields] === undefined) {
|
||||
result[field as TFields] = issue.message
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const noInputSchema = z.object({}).strict()
|
||||
export type NoInput = z.output<typeof noInputSchema>
|
||||
|
||||
export const jobIdParamsSchema = z.object({
|
||||
jobId: z.string().min(1),
|
||||
})
|
||||
|
||||
/**
|
||||
* Non-empty string identifier (used for workspace, workflow, user, table, etc.).
|
||||
* Prefer this over inline `z.string().min(1)` so error wording stays consistent
|
||||
* and refactors can centralize ID validation in one place.
|
||||
*/
|
||||
export const nonEmptyIdSchema = z.string().min(1)
|
||||
|
||||
/**
|
||||
* Non-empty `workspaceId` field. Same constraint as `nonEmptyIdSchema` with a
|
||||
* stable, human-readable message. Use to deduplicate the
|
||||
* `z.string().min(1, 'Workspace ID is required')` pattern across contracts.
|
||||
*/
|
||||
export const workspaceIdSchema = z.string().min(1, 'Workspace ID is required')
|
||||
|
||||
/**
|
||||
* Non-empty `organizationId` field. Same constraint as `nonEmptyIdSchema` with a
|
||||
* stable, human-readable message.
|
||||
*/
|
||||
export const organizationIdSchema = z.string().min(1, 'Organization ID is required')
|
||||
|
||||
/**
|
||||
* Non-empty `workflowId` field. Same constraint as `nonEmptyIdSchema` with a
|
||||
* stable, human-readable message.
|
||||
*/
|
||||
export const workflowIdSchema = z.string().min(1, 'Workflow ID is required')
|
||||
|
||||
/**
|
||||
* A `workspace_files.id` value. The column is a free-form `text` primary key, so
|
||||
* ids come in two shapes: UUID v4 (legacy rows and the `insertFileMetadata`
|
||||
* default) and the current `wf_<shortId>` form minted by the workspace upload
|
||||
* path. Both are drawn from `[A-Za-z0-9_-]`, so accept that charset rather than a
|
||||
* UUID-only schema — a `.uuid()` constraint here silently 400s every `wf_` file.
|
||||
*/
|
||||
export const workspaceFileIdSchema = z
|
||||
.string()
|
||||
.min(1, 'File ID is required')
|
||||
.max(128, 'File ID is too long')
|
||||
.regex(/^[A-Za-z0-9_-]+$/, 'Invalid file id')
|
||||
|
||||
/**
|
||||
* Reference to an image embedded in a document: either a workspace storage `key`
|
||||
* (serve-URL embeds) or a workspace file `id` (view-URL embeds) — exactly one. Shared
|
||||
* by the in-app and public inline-image routes, which resolve it within a workspace.
|
||||
*/
|
||||
export const inlineFileRefQuerySchema = z
|
||||
.object({
|
||||
key: z.string().min(1).max(512).optional(),
|
||||
fileId: workspaceFileIdSchema.optional(),
|
||||
})
|
||||
.refine((q) => (q.key ? 1 : 0) + (q.fileId ? 1 : 0) === 1, {
|
||||
message: 'Provide exactly one of `key` or `fileId`',
|
||||
})
|
||||
|
||||
/**
|
||||
* Boolean query-string primitive that correctly handles the literal strings
|
||||
* `"true"` / `"false"` (case-insensitive) in addition to real booleans.
|
||||
*
|
||||
* Do NOT use `z.coerce.boolean()` for query parameters: it coerces any
|
||||
* non-empty string to `true`, so `?flag=false` resolves to `true`. This
|
||||
* primitive treats `"false"` / `"0"` / `""` as `false` and `"true"` / `"1"`
|
||||
* as `true`, mirroring how query strings are commonly serialized by
|
||||
* frontends and CLIs.
|
||||
*
|
||||
* Real boolean inputs (e.g. when `requestJson` serializes a JS `true`) pass
|
||||
* through unchanged. Anything else fails validation with a clear message.
|
||||
*
|
||||
* Use `.optional()` / `.default(...)` at the call site, not here, so each
|
||||
* query field controls its own omission/default semantics.
|
||||
*/
|
||||
/**
|
||||
* Canonical boundary schema for `UserFile` (`apps/sim/executor/types.ts`) — the
|
||||
* shape produced by the executor and persisted in `workflowExecutionLogs.files`,
|
||||
* forwarded through tool inputs, and rendered in the logs UI. `.passthrough()`
|
||||
* tolerates legacy/extra fields on stored rows (e.g. `uploadedAt`, `expiresAt`,
|
||||
* `storageProvider`) without rejecting the whole payload.
|
||||
*/
|
||||
export const userFileSchema = z
|
||||
.object({
|
||||
id: z.string().optional().default(''),
|
||||
name: z.string().min(1),
|
||||
url: z.string().optional().default(''),
|
||||
size: z.coerce.number().nonnegative(),
|
||||
type: z.string().optional().default('application/octet-stream'),
|
||||
key: z.string().min(1),
|
||||
context: z.string().optional(),
|
||||
base64: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
/**
|
||||
* Per-stage redaction policy: which entity types to mask, in which language. An
|
||||
* enabled stage must name at least one entity type — "redact all" is not an
|
||||
* expressible policy, so `enabled: true` with an empty list (which would resolve
|
||||
* to off and silently skip masking) is rejected at the boundary.
|
||||
*/
|
||||
export const piiStagePolicySchema = z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
/** Presidio entity types to mask. Disabled stages may be empty. */
|
||||
entityTypes: z.array(z.string().min(1, 'Entity type cannot be empty')).max(100),
|
||||
/** Language whose Presidio recognizers apply; defaults to English. */
|
||||
language: z.enum(PII_LANGUAGE_CODES).optional(),
|
||||
})
|
||||
.refine((stage) => !stage.enabled || stage.entityTypes.length > 0, {
|
||||
message: 'An enabled redaction stage must select at least one entity type.',
|
||||
path: ['entityTypes'],
|
||||
})
|
||||
|
||||
export type PiiStagePolicy = z.output<typeof piiStagePolicySchema>
|
||||
|
||||
/** The three redaction stages, each independently configured. */
|
||||
export const piiStagesSchema = z.object({
|
||||
input: piiStagePolicySchema,
|
||||
blockOutputs: piiStagePolicySchema,
|
||||
logs: piiStagePolicySchema,
|
||||
})
|
||||
|
||||
export type PiiStages = z.output<typeof piiStagesSchema>
|
||||
|
||||
/**
|
||||
* A single PII redaction rule targeting one scope (all workspaces, or one).
|
||||
* New rules carry per-stage `stages`; legacy rows carry only the flat
|
||||
* `entityTypes`/`language` (resolved as logs-only). At least one must be present.
|
||||
*/
|
||||
export const piiRedactionRuleSchema = z
|
||||
.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().max(100).optional(),
|
||||
/** null = all workspaces; otherwise the single targeted workspace. */
|
||||
workspaceId: z.string().min(1).nullable(),
|
||||
/** Per-stage policy (input / blockOutputs / logs). */
|
||||
stages: piiStagesSchema.optional(),
|
||||
/** Legacy flat policy (pre-stages). Retained for back-compat parse + migration. */
|
||||
entityTypes: z.array(z.string().min(1, 'Entity type cannot be empty')).max(100).optional(),
|
||||
language: z.enum(PII_LANGUAGE_CODES).optional(),
|
||||
})
|
||||
.refine((rule) => rule.stages !== undefined || rule.entityTypes !== undefined, {
|
||||
message: 'A PII redaction rule must define either stages or entityTypes.',
|
||||
})
|
||||
|
||||
export type PiiRedactionRule = z.output<typeof piiRedactionRuleSchema>
|
||||
|
||||
/**
|
||||
* Enterprise PII redaction policy applied to workflow logs on persist. Each
|
||||
* scope is unique: at most one all-workspaces rule (`workspaceId: null`) and at
|
||||
* most one rule per workspace — resolution is most-specific-wins, so duplicate
|
||||
* scopes would make masking depend on array order.
|
||||
*/
|
||||
export const piiRedactionSettingsSchema = z.object({
|
||||
rules: z
|
||||
.array(piiRedactionRuleSchema)
|
||||
.max(1000)
|
||||
.refine(
|
||||
(rules) => {
|
||||
const scopes = rules.map((r) => r.workspaceId ?? '__all__')
|
||||
return new Set(scopes).size === scopes.length
|
||||
},
|
||||
{
|
||||
message:
|
||||
'Each workspace (and the all-workspaces default) may have at most one PII redaction rule.',
|
||||
}
|
||||
),
|
||||
})
|
||||
|
||||
export type PiiRedactionSettings = z.output<typeof piiRedactionSettingsSchema>
|
||||
|
||||
/** Retention hours bound: 1 day to ~5 years, in hours. */
|
||||
const retentionOverrideHoursSchema = z.number().int().min(24).max(43800).nullable().optional()
|
||||
|
||||
/**
|
||||
* A per-workspace override of the org retention hours. Each field is tri-state:
|
||||
* omitted = inherit the org value; a number = that workspace's retention in
|
||||
* hours; `null` = forever (never delete).
|
||||
*/
|
||||
export const retentionOverrideSchema = z.object({
|
||||
workspaceId: workspaceIdSchema,
|
||||
logRetentionHours: retentionOverrideHoursSchema,
|
||||
softDeleteRetentionHours: retentionOverrideHoursSchema,
|
||||
taskCleanupHours: retentionOverrideHoursSchema,
|
||||
})
|
||||
|
||||
export type RetentionOverride = z.output<typeof retentionOverrideSchema>
|
||||
|
||||
/**
|
||||
* Per-workspace retention overrides. Each workspace appears at most once —
|
||||
* resolution is workspace-override-then-org-default, so duplicate workspaces
|
||||
* would make the effective value depend on array order.
|
||||
*/
|
||||
export const retentionOverridesSchema = z
|
||||
.array(retentionOverrideSchema)
|
||||
.max(1000)
|
||||
.refine(
|
||||
(overrides) => {
|
||||
const ids = overrides.map((o) => o.workspaceId)
|
||||
return new Set(ids).size === ids.length
|
||||
},
|
||||
{ message: 'Each workspace may have at most one retention override.' }
|
||||
)
|
||||
|
||||
export type RetentionOverrides = z.output<typeof retentionOverridesSchema>
|
||||
|
||||
export const booleanQueryFlagSchema = z.preprocess(
|
||||
(value) => {
|
||||
if (typeof value === 'boolean') return value
|
||||
if (typeof value !== 'string') return value
|
||||
const normalized = value.trim().toLowerCase()
|
||||
if (normalized === 'true' || normalized === '1') return true
|
||||
if (normalized === 'false' || normalized === '0' || normalized === '') return false
|
||||
return value
|
||||
},
|
||||
z.boolean({ error: 'must be a boolean (true/false)' })
|
||||
)
|
||||
@@ -0,0 +1,337 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const openRouterModelInfoSchema = z.object({
|
||||
id: z.string(),
|
||||
contextLength: z.number().optional(),
|
||||
supportsStructuredOutputs: z.boolean().optional(),
|
||||
supportsTools: z.boolean().optional(),
|
||||
pricing: z
|
||||
.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const providerModelsResponseSchema = z.object({
|
||||
models: z.array(z.string()),
|
||||
modelInfo: z.record(z.string(), openRouterModelInfoSchema).optional(),
|
||||
})
|
||||
export type ProviderModelsResponse = z.output<typeof providerModelsResponseSchema>
|
||||
|
||||
export const fireworksProviderModelsQuerySchema = z.object({
|
||||
workspaceId: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
export const ollamaCloudProviderModelsQuerySchema = z.object({
|
||||
workspaceId: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
export const togetherProviderModelsQuerySchema = z.object({
|
||||
workspaceId: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
export const basetenProviderModelsQuerySchema = z.object({
|
||||
workspaceId: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
export const openRouterUpstreamResponseSchema = z.object({
|
||||
data: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
id: z.string(),
|
||||
context_length: z.number().optional(),
|
||||
supported_parameters: z.array(z.string()).optional(),
|
||||
pricing: z
|
||||
.object({
|
||||
prompt: z.string().optional(),
|
||||
completion: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.optional(),
|
||||
})
|
||||
.passthrough()
|
||||
)
|
||||
.default([]),
|
||||
})
|
||||
|
||||
export const vllmUpstreamResponseSchema = z.object({
|
||||
data: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
id: z.string(),
|
||||
})
|
||||
.passthrough()
|
||||
)
|
||||
.default([]),
|
||||
})
|
||||
|
||||
export const fireworksUpstreamResponseSchema = z.object({
|
||||
data: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
id: z.string(),
|
||||
object: z.string().optional(),
|
||||
created: z.number().optional(),
|
||||
owned_by: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
)
|
||||
.default([]),
|
||||
object: z.string().optional(),
|
||||
})
|
||||
|
||||
const togetherModelObjectSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
object: z.string().optional(),
|
||||
created: z.number().optional(),
|
||||
type: z.string().optional(),
|
||||
display_name: z.string().optional(),
|
||||
organization: z.string().optional(),
|
||||
context_length: z.number().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
/** Together's `GET /v1/models` returns a bare top-level array of model objects. */
|
||||
export const togetherUpstreamResponseSchema = z.array(togetherModelObjectSchema)
|
||||
|
||||
export const basetenUpstreamResponseSchema = z.object({
|
||||
data: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
id: z.string(),
|
||||
object: z.string().optional(),
|
||||
created: z.number().optional(),
|
||||
owned_by: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
)
|
||||
.default([]),
|
||||
object: z.string().optional(),
|
||||
})
|
||||
|
||||
// Shared by the local Ollama and Ollama Cloud /api/tags endpoints — same `{ models: [{ name }] }` shape.
|
||||
export const ollamaUpstreamResponseSchema = z.object({
|
||||
models: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
name: z.string(),
|
||||
})
|
||||
.passthrough()
|
||||
)
|
||||
.default([]),
|
||||
})
|
||||
|
||||
const providerToolSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
params: z.record(z.string(), z.unknown()),
|
||||
parameters: z
|
||||
.object({
|
||||
type: z.string(),
|
||||
properties: z.record(z.string(), z.unknown()),
|
||||
required: z.array(z.string()),
|
||||
})
|
||||
.passthrough(),
|
||||
usageControl: z.enum(['auto', 'force', 'none']).optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const providerMessageSchema = z
|
||||
.object({
|
||||
role: z.enum(['system', 'user', 'assistant', 'function', 'tool']),
|
||||
content: z.string().nullable(),
|
||||
name: z.string().optional(),
|
||||
function_call: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
arguments: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
tool_calls: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal('function'),
|
||||
function: z.object({
|
||||
name: z.string(),
|
||||
arguments: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
tool_call_id: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const providerResponseFormatSchema = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
// untyped-response: caller-supplied JSON Schema (request body field, not a route response)
|
||||
schema: z.unknown(),
|
||||
strict: z.boolean().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const providerApiRequestBodySchema = z
|
||||
.object({
|
||||
provider: z.string().min(1),
|
||||
model: z.string().min(1),
|
||||
systemPrompt: z.string().optional(),
|
||||
context: z.string().optional(),
|
||||
tools: z.array(providerToolSchema).optional(),
|
||||
temperature: z.number().optional(),
|
||||
maxTokens: z.number().optional(),
|
||||
apiKey: z.string().optional(),
|
||||
azureEndpoint: z.string().optional(),
|
||||
azureApiVersion: z.string().optional(),
|
||||
vertexProject: z.string().optional(),
|
||||
vertexLocation: z.string().optional(),
|
||||
vertexCredential: z.string().optional(),
|
||||
bedrockAccessKeyId: z.string().optional(),
|
||||
bedrockSecretKey: z.string().optional(),
|
||||
bedrockRegion: z.string().optional(),
|
||||
responseFormat: providerResponseFormatSchema.optional(),
|
||||
workflowId: z.string().optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
stream: z.boolean().optional(),
|
||||
messages: z.array(providerMessageSchema).optional(),
|
||||
environmentVariables: z.record(z.string(), z.string()).optional(),
|
||||
workflowVariables: z.record(z.string(), z.unknown()).optional(),
|
||||
blockData: z.record(z.string(), z.unknown()).optional(),
|
||||
blockNameMapping: z.record(z.string(), z.string()).optional(),
|
||||
reasoningEffort: z.string().optional(),
|
||||
verbosity: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
export type ProviderApiRequestBody = z.input<typeof providerApiRequestBodySchema>
|
||||
|
||||
export const getBaseProviderModelsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/providers/base/models',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: providerModelsResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getOllamaProviderModelsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/providers/ollama/models',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: providerModelsResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getVllmProviderModelsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/providers/vllm/models',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: providerModelsResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getOpenRouterProviderModelsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/providers/openrouter/models',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: providerModelsResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getLitellmProviderModelsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/providers/litellm/models',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: providerModelsResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getFireworksProviderModelsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/providers/fireworks/models',
|
||||
query: fireworksProviderModelsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: providerModelsResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getOllamaCloudProviderModelsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/providers/ollama-cloud/models',
|
||||
query: ollamaCloudProviderModelsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: providerModelsResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getTogetherProviderModelsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/providers/together/models',
|
||||
query: togetherProviderModelsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: providerModelsResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getBasetenProviderModelsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/providers/baseten/models',
|
||||
query: basetenProviderModelsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: providerModelsResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* `POST /api/providers` returns either a streamed response (handled at the
|
||||
* runtime level — this contract models only the JSON case) or a JSON provider
|
||||
* payload. The JSON case mirrors the canonical `ProviderResponse` shape from
|
||||
* `@/providers/types`, but provider-specific fields are tolerated via
|
||||
* passthrough so raw provider output flows through without contract drift.
|
||||
*/
|
||||
const executeProviderResponseSchema = z
|
||||
.object({
|
||||
content: z.string(),
|
||||
model: z.string(),
|
||||
tokens: z
|
||||
.object({
|
||||
input: z.number().optional(),
|
||||
output: z.number().optional(),
|
||||
total: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
toolCalls: z.array(z.record(z.string(), z.unknown())).optional(),
|
||||
toolResults: z.array(z.record(z.string(), z.unknown())).optional(),
|
||||
timing: z.record(z.string(), z.unknown()).optional(),
|
||||
cost: z.record(z.string(), z.unknown()).optional(),
|
||||
interactionId: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const executeProviderContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/providers',
|
||||
body: providerApiRequestBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: executeProviderResponseSchema,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,240 @@
|
||||
import { z } from 'zod'
|
||||
import { inlineFileRefQuerySchema, workspaceIdSchema } from '@/lib/api/contracts/primitives'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const shareResourceTypeSchema = z.enum(['file', 'folder'])
|
||||
|
||||
/** How a public share is gated. */
|
||||
export const shareAuthTypeSchema = z.enum(['public', 'password', 'email', 'sso'])
|
||||
|
||||
export type ShareAuthType = z.output<typeof shareAuthTypeSchema>
|
||||
|
||||
/** An allowed email address or `@domain` pattern for email/SSO shares. */
|
||||
const allowedEmailSchema = z.string().min(1).max(320)
|
||||
|
||||
/**
|
||||
* Public-safe representation of a `public_share` row. Never carries the
|
||||
* underlying storage key or the (encrypted) password — `hasPassword` is the
|
||||
* only password signal exposed to clients. `allowedEmails` is the allow-list for
|
||||
* email/SSO shares (visible only to workspace members via the authed share route).
|
||||
*/
|
||||
export const shareRecordSchema = z.object({
|
||||
id: z.string(),
|
||||
token: z.string(),
|
||||
url: z.string(),
|
||||
isActive: z.boolean(),
|
||||
resourceType: shareResourceTypeSchema,
|
||||
resourceId: z.string(),
|
||||
authType: shareAuthTypeSchema,
|
||||
hasPassword: z.boolean(),
|
||||
allowedEmails: z.array(allowedEmailSchema),
|
||||
})
|
||||
|
||||
export type ShareRecord = z.output<typeof shareRecordSchema>
|
||||
|
||||
const fileShareParamsSchema = z.object({
|
||||
id: workspaceIdSchema,
|
||||
fileId: z.string().min(1, 'File ID is required'),
|
||||
})
|
||||
|
||||
export const upsertFileShareBodySchema = z.object({
|
||||
isActive: z.boolean(),
|
||||
authType: shareAuthTypeSchema.optional(),
|
||||
password: z
|
||||
.string()
|
||||
.min(1, 'Password cannot be empty')
|
||||
.max(1024, 'Password is too long')
|
||||
.optional(),
|
||||
allowedEmails: z.array(allowedEmailSchema).max(200, 'Too many allowed emails').optional(),
|
||||
// Client-reserved token shown as the link before saving; persisted on first
|
||||
// enable so a copied link resolves. Ignored once the share row already exists.
|
||||
token: z
|
||||
.string()
|
||||
.regex(/^[A-Za-z0-9_-]+$/, 'Invalid token')
|
||||
.min(16, 'Token is too short')
|
||||
.max(64, 'Token is too long')
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type UpsertFileShareBody = z.input<typeof upsertFileShareBodySchema>
|
||||
|
||||
const getFileShareResponseSchema = z.object({
|
||||
share: shareRecordSchema.nullable(),
|
||||
})
|
||||
|
||||
export type GetFileShareResponse = z.output<typeof getFileShareResponseSchema>
|
||||
|
||||
export const getFileShareContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/workspaces/[id]/files/[fileId]/share',
|
||||
params: fileShareParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: getFileShareResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
const upsertFileShareResponseSchema = z.object({
|
||||
share: shareRecordSchema,
|
||||
})
|
||||
|
||||
export type UpsertFileShareResponse = z.output<typeof upsertFileShareResponseSchema>
|
||||
|
||||
export const upsertFileShareContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/workspaces/[id]/files/[fileId]/share',
|
||||
params: fileShareParamsSchema,
|
||||
body: upsertFileShareBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: upsertFileShareResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const publicFileTokenParamsSchema = z.object({
|
||||
token: z.string().min(1, 'Token is required'),
|
||||
})
|
||||
|
||||
const publicFileMetadataSchema = z.object({
|
||||
token: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
size: z.number(),
|
||||
workspaceName: z.string().nullable(),
|
||||
ownerName: z.string().nullable(),
|
||||
})
|
||||
|
||||
export type PublicFileMetadata = z.output<typeof publicFileMetadataSchema>
|
||||
|
||||
export const getPublicFileContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/files/public/[token]',
|
||||
params: publicFileTokenParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: publicFileMetadataSchema,
|
||||
},
|
||||
})
|
||||
|
||||
/** Binary stream of the shared file's bytes. Authorized solely by an active token. */
|
||||
export const getPublicFileContentContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/files/public/[token]/content',
|
||||
params: publicFileTokenParamsSchema,
|
||||
response: {
|
||||
mode: 'binary',
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Binary stream of an image embedded in a shared document. Authorized by the parent
|
||||
* document's active share — the route serves the bytes only when the reference is
|
||||
* actually embedded in the shared document AND the file lives in the same workspace,
|
||||
* and only when the bytes are a renderable raster image.
|
||||
*/
|
||||
export const getPublicInlineFileContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/files/public/[token]/inline',
|
||||
params: publicFileTokenParamsSchema,
|
||||
query: inlineFileRefQuerySchema,
|
||||
response: {
|
||||
mode: 'binary',
|
||||
},
|
||||
})
|
||||
|
||||
const authenticatePublicFileBodySchema = z.object({
|
||||
password: z.string().min(1, 'Password is required').max(1024, 'Password is too long'),
|
||||
})
|
||||
|
||||
export type AuthenticatePublicFileBody = z.input<typeof authenticatePublicFileBodySchema>
|
||||
|
||||
const authenticatePublicFileResponseSchema = z.object({
|
||||
authType: shareAuthTypeSchema,
|
||||
})
|
||||
|
||||
export type AuthenticatePublicFileResponse = z.output<typeof authenticatePublicFileResponseSchema>
|
||||
|
||||
/**
|
||||
* Exchanges a share password for a `file_auth_{shareId}` cookie. IP rate-limited;
|
||||
* returns 401 (`Invalid password`) on mismatch and 429 when throttled.
|
||||
*/
|
||||
export const authenticatePublicFileContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/public/[token]',
|
||||
params: publicFileTokenParamsSchema,
|
||||
body: authenticatePublicFileBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: authenticatePublicFileResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
const requestPublicFileOtpBodySchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
})
|
||||
|
||||
export type RequestPublicFileOtpBody = z.input<typeof requestPublicFileOtpBodySchema>
|
||||
|
||||
const requestPublicFileOtpResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
})
|
||||
|
||||
/** Sends a 6-digit verification code to an allow-listed email for an email-gated share. */
|
||||
export const requestPublicFileOtpContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/public/[token]/otp',
|
||||
params: publicFileTokenParamsSchema,
|
||||
body: requestPublicFileOtpBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: requestPublicFileOtpResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
const verifyPublicFileOtpBodySchema = requestPublicFileOtpBodySchema.extend({
|
||||
otp: z.string().length(6, 'Verification code must be 6 digits'),
|
||||
})
|
||||
|
||||
export type VerifyPublicFileOtpBody = z.input<typeof verifyPublicFileOtpBodySchema>
|
||||
|
||||
const verifyPublicFileOtpResponseSchema = z.object({
|
||||
authType: shareAuthTypeSchema,
|
||||
})
|
||||
|
||||
export type VerifyPublicFileOtpResponse = z.output<typeof verifyPublicFileOtpResponseSchema>
|
||||
|
||||
/** Verifies the OTP and, on success, sets the `file_auth_{shareId}` cookie. */
|
||||
export const verifyPublicFileOtpContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/files/public/[token]/otp',
|
||||
params: publicFileTokenParamsSchema,
|
||||
body: verifyPublicFileOtpBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: verifyPublicFileOtpResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
const publicFileSSOBodySchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
})
|
||||
|
||||
export type PublicFileSSOBody = z.input<typeof publicFileSSOBodySchema>
|
||||
|
||||
const publicFileSSOResponseSchema = z.object({
|
||||
eligible: z.boolean(),
|
||||
})
|
||||
|
||||
export type PublicFileSSOResponse = z.output<typeof publicFileSSOResponseSchema>
|
||||
|
||||
/** Reports whether an email is on the allow-list for an SSO-gated share. */
|
||||
export const publicFileSSOContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/public/[token]/sso',
|
||||
params: publicFileTokenParamsSchema,
|
||||
body: publicFileSSOBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: publicFileSSOResponseSchema,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,324 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const scheduleStatusSchema = z.enum(['active', 'disabled', 'completed'])
|
||||
export type ScheduleStatus = z.output<typeof scheduleStatusSchema>
|
||||
|
||||
export const scheduleSourceTypeSchema = z.enum(['workflow', 'job'])
|
||||
export type ScheduleSourceType = z.output<typeof scheduleSourceTypeSchema>
|
||||
|
||||
export const scheduleLifecycleSchema = z.enum(['persistent', 'until_complete'])
|
||||
export type ScheduleLifecycle = z.output<typeof scheduleLifecycleSchema>
|
||||
|
||||
/**
|
||||
* A `@`-mentioned resource or `/`-invoked skill captured with a scheduled
|
||||
* task's prompt. `kind` discriminates the variant and the remaining keys carry
|
||||
* the variant-specific identifiers (`workflowId`, `tableId`, `skillId`, …), so
|
||||
* the shape is intentionally open beyond the always-present `kind`/`label`.
|
||||
*/
|
||||
export const scheduleContextSchema = z
|
||||
.object({ kind: z.string().min(1), label: z.string() })
|
||||
.passthrough()
|
||||
|
||||
export type ScheduleContext = z.output<typeof scheduleContextSchema>
|
||||
|
||||
export const scheduleIdParamsSchema = z.object({
|
||||
id: z.string().min(1, 'Invalid schedule ID'),
|
||||
})
|
||||
|
||||
export const scheduleQuerySchema = z.object({
|
||||
workflowId: z.string().optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
blockId: z.string().optional(),
|
||||
})
|
||||
|
||||
const workflowScheduleQuerySchema = z.object({
|
||||
workflowId: z.string().min(1).optional(),
|
||||
blockId: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Mirrors a full `workflow_schedule` row as it appears on the wire after
|
||||
* `NextResponse.json` serialization. Single-schedule and workspace-list
|
||||
* responses both spread the full row, so the schema describes every column
|
||||
* (with NOT NULL columns required and timestamps as ISO strings).
|
||||
*/
|
||||
export const workflowScheduleRowSchema = z.object({
|
||||
id: z.string(),
|
||||
workflowId: z.string().nullable(),
|
||||
deploymentVersionId: z.string().nullable(),
|
||||
blockId: z.string().nullable(),
|
||||
cronExpression: z.string().nullable(),
|
||||
nextRunAt: z.string().nullable(),
|
||||
lastRanAt: z.string().nullable(),
|
||||
lastQueuedAt: z.string().nullable(),
|
||||
triggerType: z.string(),
|
||||
timezone: z.string(),
|
||||
failedCount: z.number(),
|
||||
infraRetryCount: z.number(),
|
||||
status: scheduleStatusSchema,
|
||||
lastFailedAt: z.string().nullable(),
|
||||
/**
|
||||
* Legacy rows pre-dating the `sourceType` column can still surface in
|
||||
* workspace listings via `isNull(sourceType)` filters, so the wire may emit
|
||||
* `null` even though the column is `notNull` for new rows.
|
||||
*/
|
||||
sourceType: scheduleSourceTypeSchema.nullable(),
|
||||
jobTitle: z.string().nullable(),
|
||||
prompt: z.string().nullable(),
|
||||
lifecycle: scheduleLifecycleSchema,
|
||||
successCondition: z.string().nullable(),
|
||||
maxRuns: z.number().nullable(),
|
||||
runCount: z.number(),
|
||||
sourceChatId: z.string().nullable(),
|
||||
sourceTaskName: z.string().nullable(),
|
||||
sourceUserId: z.string().nullable(),
|
||||
sourceWorkspaceId: z.string().nullable(),
|
||||
jobHistory: z.array(z.object({ timestamp: z.string(), summary: z.string() })).nullable(),
|
||||
contexts: z.array(scheduleContextSchema).nullable(),
|
||||
excludedDates: z.array(z.string()).nullable(),
|
||||
endsAt: z.string().nullable(),
|
||||
archivedAt: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
|
||||
export type WorkflowScheduleRow = z.output<typeof workflowScheduleRowSchema>
|
||||
|
||||
/**
|
||||
* Workspace-scope listing extends the row with synthesized join fields.
|
||||
* Workflow-backed rows carry the workflow name from `workflow` (NOT NULL
|
||||
* column); job-backed rows synthesize `null` server-side.
|
||||
*/
|
||||
export const workspaceScheduleRowSchema = workflowScheduleRowSchema.extend({
|
||||
workflowName: z.string().nullable(),
|
||||
})
|
||||
|
||||
export type WorkspaceScheduleRow = z.output<typeof workspaceScheduleRowSchema>
|
||||
|
||||
export const createScheduleBodySchema = z
|
||||
.object({
|
||||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
||||
title: z.string().min(1, 'Title is required'),
|
||||
prompt: z.string().min(1, 'Prompt is required'),
|
||||
/** Recurring cadence. Omit (with `time` set) for a one-time task. */
|
||||
cronExpression: z.string().min(1).optional(),
|
||||
/** One-time launch instant (ISO 8601). Omit (with `cronExpression` set) for a recurring task. */
|
||||
time: z.string().min(1).optional(),
|
||||
timezone: z.string().optional().default('UTC'),
|
||||
lifecycle: scheduleLifecycleSchema.optional().default('persistent'),
|
||||
/** Recurrence end after N runs (gcal "ends after N occurrences"). */
|
||||
maxRuns: z.number().int().positive().optional(),
|
||||
/** Recurrence end on a date (ISO 8601; gcal "ends on date"). */
|
||||
endsAt: z.string().optional(),
|
||||
startDate: z.string().optional(),
|
||||
contexts: z.array(scheduleContextSchema).optional(),
|
||||
})
|
||||
.superRefine((body, ctx) => {
|
||||
if (!body.cronExpression && !body.time) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['time'],
|
||||
message: 'Provide a cron expression for a recurring task or a time for a one-time task',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export type CreateScheduleBody = z.input<typeof createScheduleBodySchema>
|
||||
|
||||
export const reactivateScheduleBodySchema = z.object({
|
||||
action: z.literal('reactivate'),
|
||||
})
|
||||
|
||||
export type ReactivateScheduleBody = z.input<typeof reactivateScheduleBodySchema>
|
||||
|
||||
export const disableScheduleBodySchema = z.object({
|
||||
action: z.literal('disable'),
|
||||
})
|
||||
|
||||
export type DisableScheduleBody = z.input<typeof disableScheduleBodySchema>
|
||||
|
||||
export const updateScheduleBodySchema = z.object({
|
||||
action: z.literal('update'),
|
||||
title: z.string().min(1).optional(),
|
||||
prompt: z.string().min(1).optional(),
|
||||
cronExpression: z.string().nullable().optional(),
|
||||
/** One-time launch instant (ISO 8601). Switches a task to one-time when set alongside a null `cronExpression`. */
|
||||
time: z.string().min(1).optional(),
|
||||
timezone: z.string().optional(),
|
||||
lifecycle: scheduleLifecycleSchema.optional(),
|
||||
maxRuns: z.number().int().positive().nullable().optional(),
|
||||
endsAt: z.string().nullable().optional(),
|
||||
contexts: z.array(scheduleContextSchema).optional(),
|
||||
})
|
||||
|
||||
export type UpdateScheduleBody = z.input<typeof updateScheduleBodySchema>
|
||||
|
||||
/**
|
||||
* Deletes a single occurrence of a recurring task (gcal "this event"): the
|
||||
* occurrence's instant is added to the schedule's exclusion list and the next
|
||||
* run advances past it. Deleting the whole series uses {@link deleteScheduleContract}.
|
||||
*/
|
||||
export const excludeOccurrenceBodySchema = z.object({
|
||||
action: z.literal('exclude_occurrence'),
|
||||
occurrence: z.string().min(1, 'Occurrence timestamp is required'),
|
||||
})
|
||||
|
||||
export type ExcludeOccurrenceBody = z.input<typeof excludeOccurrenceBodySchema>
|
||||
|
||||
export const scheduleUpdateSchema = z.discriminatedUnion('action', [
|
||||
reactivateScheduleBodySchema,
|
||||
disableScheduleBodySchema,
|
||||
updateScheduleBodySchema,
|
||||
excludeOccurrenceBodySchema,
|
||||
])
|
||||
|
||||
export type ScheduleUpdate = z.input<typeof scheduleUpdateSchema>
|
||||
|
||||
const messageResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
nextRunAt: z.string().optional(),
|
||||
})
|
||||
|
||||
export const executeSchedulesResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
status: z.literal('started'),
|
||||
})
|
||||
|
||||
export type ExecuteSchedulesResponse = z.output<typeof executeSchedulesResponseSchema>
|
||||
|
||||
export const getScheduleContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/schedules',
|
||||
query: workflowScheduleQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
schedule: workflowScheduleRowSchema.nullable(),
|
||||
isDisabled: z.boolean().optional(),
|
||||
hasFailures: z.boolean().optional(),
|
||||
canBeReactivated: z.boolean().optional(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const listWorkspaceSchedulesContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/schedules',
|
||||
query: z.object({
|
||||
workspaceId: z.string().min(1),
|
||||
}),
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
schedules: z.array(workspaceScheduleRowSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Single-schedule read by id. Used by the mothership resource viewer so opening
|
||||
* a scheduled-task artifact does a lightweight by-id fetch instead of pulling
|
||||
* the entire workspace schedule list (which contended with the chat stream).
|
||||
*/
|
||||
export const getScheduleByIdContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/schedules/[id]',
|
||||
params: scheduleIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
schedule: workflowScheduleRowSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Newly-created job schedules emit a partial summary with the canonical fields
|
||||
* the route synthesizes server-side; everything else is filled in on
|
||||
* subsequent reads.
|
||||
*/
|
||||
export const createScheduleResponseSchema = z.object({
|
||||
schedule: z.object({
|
||||
id: z.string(),
|
||||
status: scheduleStatusSchema,
|
||||
/** Null for one-time tasks, which carry no recurring cadence. */
|
||||
cronExpression: z.string().nullable(),
|
||||
nextRunAt: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
export type CreateScheduleResponse = z.output<typeof createScheduleResponseSchema>
|
||||
|
||||
export const createScheduleContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/schedules',
|
||||
body: createScheduleBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: createScheduleResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const reactivateScheduleContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/schedules/[id]',
|
||||
params: scheduleIdParamsSchema,
|
||||
body: reactivateScheduleBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: messageResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const disableScheduleContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/schedules/[id]',
|
||||
params: scheduleIdParamsSchema,
|
||||
body: disableScheduleBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: messageResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateScheduleContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/schedules/[id]',
|
||||
params: scheduleIdParamsSchema,
|
||||
body: scheduleUpdateSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: messageResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const excludeOccurrenceContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/schedules/[id]',
|
||||
params: scheduleIdParamsSchema,
|
||||
body: excludeOccurrenceBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: messageResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteScheduleContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/schedules/[id]',
|
||||
params: scheduleIdParamsSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: messageResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const executeSchedulesContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/schedules/execute',
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: executeSchedulesResponseSchema,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialWorkflowBodySchema,
|
||||
definePostSelector,
|
||||
idNameSchema,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
|
||||
const airtableBaseSchema = idNameSchema
|
||||
const airtableTableSchema = idNameSchema
|
||||
|
||||
export const airtableTablesBodySchema = credentialWorkflowBodySchema.extend({
|
||||
baseId: z.string().min(1, 'Base ID is required'),
|
||||
})
|
||||
|
||||
export const airtableBasesSelectorContract = definePostSelector(
|
||||
'/api/tools/airtable/bases',
|
||||
credentialWorkflowBodySchema.passthrough(),
|
||||
z.object({ bases: z.array(airtableBaseSchema) })
|
||||
)
|
||||
|
||||
export const airtableTablesSelectorContract = definePostSelector(
|
||||
'/api/tools/airtable/tables',
|
||||
airtableTablesBodySchema,
|
||||
z.object({ tables: z.array(airtableTableSchema) })
|
||||
)
|
||||
|
||||
export type AirtableBasesSelectorResponse = ContractJsonResponse<
|
||||
typeof airtableBasesSelectorContract
|
||||
>
|
||||
export type AirtableTablesSelectorResponse = ContractJsonResponse<
|
||||
typeof airtableTablesSelectorContract
|
||||
>
|
||||
@@ -0,0 +1,19 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialWorkflowBodySchema,
|
||||
definePostSelector,
|
||||
idNameSchema,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
|
||||
const asanaWorkspaceSchema = idNameSchema
|
||||
|
||||
export const asanaWorkspacesSelectorContract = definePostSelector(
|
||||
'/api/tools/asana/workspaces',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ workspaces: z.array(asanaWorkspaceSchema) })
|
||||
)
|
||||
|
||||
export type AsanaWorkspacesSelectorResponse = ContractJsonResponse<
|
||||
typeof asanaWorkspacesSelectorContract
|
||||
>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialWorkflowBodySchema,
|
||||
definePostSelector,
|
||||
idNameSchema,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
|
||||
const attioObjectSchema = idNameSchema
|
||||
const attioListSchema = idNameSchema
|
||||
|
||||
export const attioObjectsSelectorContract = definePostSelector(
|
||||
'/api/tools/attio/objects',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ objects: z.array(attioObjectSchema) })
|
||||
)
|
||||
|
||||
export const attioListsSelectorContract = definePostSelector(
|
||||
'/api/tools/attio/lists',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ lists: z.array(attioListSchema) })
|
||||
)
|
||||
|
||||
export type AttioObjectsSelectorResponse = ContractJsonResponse<typeof attioObjectsSelectorContract>
|
||||
export type AttioListsSelectorResponse = ContractJsonResponse<typeof attioListsSelectorContract>
|
||||
@@ -0,0 +1,57 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialWorkflowImpersonateBodySchema,
|
||||
definePostSelector,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractBodyInput, ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
|
||||
const bigQueryDatasetSchema = z
|
||||
.object({
|
||||
datasetReference: z
|
||||
.object({
|
||||
datasetId: z.string(),
|
||||
projectId: z.string(),
|
||||
})
|
||||
.passthrough(),
|
||||
friendlyName: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const bigQueryTableSchema = z
|
||||
.object({
|
||||
tableReference: z.object({ tableId: z.string() }).passthrough(),
|
||||
friendlyName: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const bigQueryDatasetsBodySchema = credentialWorkflowImpersonateBodySchema.extend({
|
||||
projectId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const bigQueryTablesBodySchema = bigQueryDatasetsBodySchema.extend({
|
||||
datasetId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const bigQueryDatasetsSelectorContract = definePostSelector(
|
||||
'/api/tools/google_bigquery/datasets',
|
||||
bigQueryDatasetsBodySchema,
|
||||
z.object({ datasets: z.array(bigQueryDatasetSchema) })
|
||||
)
|
||||
|
||||
export const bigQueryTablesSelectorContract = definePostSelector(
|
||||
'/api/tools/google_bigquery/tables',
|
||||
bigQueryTablesBodySchema,
|
||||
z.object({ tables: z.array(bigQueryTableSchema) })
|
||||
)
|
||||
|
||||
export type BigQueryDatasetsSelectorBody = ContractBodyInput<
|
||||
typeof bigQueryDatasetsSelectorContract
|
||||
>
|
||||
export type BigQueryTablesSelectorBody = ContractBodyInput<typeof bigQueryTablesSelectorContract>
|
||||
|
||||
export type BigQueryDatasetsSelectorResponse = ContractJsonResponse<
|
||||
typeof bigQueryDatasetsSelectorContract
|
||||
>
|
||||
export type BigQueryTablesSelectorResponse = ContractJsonResponse<
|
||||
typeof bigQueryTablesSelectorContract
|
||||
>
|
||||
@@ -0,0 +1,30 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialWorkflowBodySchema,
|
||||
definePostSelector,
|
||||
idNameSchema,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
|
||||
const calcomEventTypeSchema = z
|
||||
.object({ id: z.string(), title: z.string(), slug: z.string() })
|
||||
.passthrough()
|
||||
|
||||
export const calcomEventTypesSelectorContract = definePostSelector(
|
||||
'/api/tools/calcom/event-types',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ eventTypes: z.array(calcomEventTypeSchema) })
|
||||
)
|
||||
|
||||
export const calcomSchedulesSelectorContract = definePostSelector(
|
||||
'/api/tools/calcom/schedules',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ schedules: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
export type CalcomEventTypesSelectorResponse = ContractJsonResponse<
|
||||
typeof calcomEventTypesSelectorContract
|
||||
>
|
||||
export type CalcomSchedulesSelectorResponse = ContractJsonResponse<
|
||||
typeof calcomSchedulesSelectorContract
|
||||
>
|
||||
@@ -0,0 +1,84 @@
|
||||
import { z } from 'zod'
|
||||
import { definePostSelector, optionalString } from '@/lib/api/contracts/selectors/shared'
|
||||
import type {
|
||||
ContractBody,
|
||||
ContractBodyInput,
|
||||
ContractJsonResponse,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import { validateAwsRegion } from '@/lib/core/security/input-validation'
|
||||
|
||||
const cloudwatchLogGroupSchema = z.object({ logGroupName: z.string() }).passthrough()
|
||||
const cloudwatchLogStreamSchema = z.object({ logStreamName: z.string() }).passthrough()
|
||||
|
||||
/**
|
||||
* AWS region with format validation. Matches the route-level check via
|
||||
* `validateAwsRegion` (e.g. `us-east-1`, `eu-west-2`).
|
||||
*/
|
||||
const awsRegionSchema = z
|
||||
.string()
|
||||
.min(1, 'AWS region is required')
|
||||
.refine((value) => validateAwsRegion(value).isValid, {
|
||||
message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)',
|
||||
})
|
||||
|
||||
/**
|
||||
* Optional integer limit that accepts numbers, numeric strings, empty strings,
|
||||
* and null. Empty/null/undefined → undefined (no limit).
|
||||
*/
|
||||
const optionalLimitSchema = z.preprocess(
|
||||
(value) => (value === '' || value === undefined || value === null ? undefined : value),
|
||||
z.coerce.number().int().positive().optional()
|
||||
)
|
||||
|
||||
export const cloudwatchLogGroupsBodySchema = z.object({
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
region: awsRegionSchema,
|
||||
prefix: optionalString,
|
||||
limit: optionalLimitSchema.optional(),
|
||||
})
|
||||
|
||||
export const cloudwatchLogStreamsBodySchema = cloudwatchLogGroupsBodySchema.extend({
|
||||
logGroupName: z.string().min(1, 'Log group name is required'),
|
||||
})
|
||||
|
||||
export const cloudwatchLogGroupsSelectorContract = definePostSelector(
|
||||
'/api/tools/cloudwatch/describe-log-groups',
|
||||
cloudwatchLogGroupsBodySchema,
|
||||
z
|
||||
.object({
|
||||
success: z.boolean().optional(),
|
||||
output: z.object({ logGroups: z.array(cloudwatchLogGroupSchema) }),
|
||||
})
|
||||
.passthrough()
|
||||
)
|
||||
|
||||
export const cloudwatchLogStreamsSelectorContract = definePostSelector(
|
||||
'/api/tools/cloudwatch/describe-log-streams',
|
||||
cloudwatchLogStreamsBodySchema,
|
||||
z
|
||||
.object({
|
||||
success: z.boolean().optional(),
|
||||
output: z.object({ logStreams: z.array(cloudwatchLogStreamSchema) }),
|
||||
})
|
||||
.passthrough()
|
||||
)
|
||||
|
||||
export type CloudwatchLogGroupsSelectorResponse = ContractJsonResponse<
|
||||
typeof cloudwatchLogGroupsSelectorContract
|
||||
>
|
||||
export type CloudwatchLogStreamsSelectorResponse = ContractJsonResponse<
|
||||
typeof cloudwatchLogStreamsSelectorContract
|
||||
>
|
||||
export type CloudwatchLogGroupsSelectorRequest = ContractBodyInput<
|
||||
typeof cloudwatchLogGroupsSelectorContract
|
||||
>
|
||||
export type CloudwatchLogGroupsSelectorBody = ContractBody<
|
||||
typeof cloudwatchLogGroupsSelectorContract
|
||||
>
|
||||
export type CloudwatchLogStreamsSelectorRequest = ContractBodyInput<
|
||||
typeof cloudwatchLogStreamsSelectorContract
|
||||
>
|
||||
export type CloudwatchLogStreamsSelectorBody = ContractBody<
|
||||
typeof cloudwatchLogStreamsSelectorContract
|
||||
>
|
||||
@@ -0,0 +1,613 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialWorkflowDomainBodySchema,
|
||||
definePostSelector,
|
||||
fileOptionSchema,
|
||||
optionalString,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractBody, ContractJsonResponse, ContractQuery } from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
|
||||
const confluenceSpaceSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
key: z.string(),
|
||||
status: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const confluencePagesBodySchema = z.object({
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: optionalString,
|
||||
title: optionalString,
|
||||
limit: z.number().int().positive().optional().default(50),
|
||||
})
|
||||
|
||||
/**
|
||||
* Refines a `pageId` field to match Confluence's alphanumeric format
|
||||
* (max 255 chars). Used as a `superRefine` so multiple
|
||||
* methods (POST/PUT/DELETE) on `/api/tools/confluence/page` can share it.
|
||||
*/
|
||||
export function refineConfluencePageId(data: { pageId: string }, ctx: z.RefinementCtx): void {
|
||||
const validation = validateAlphanumericId(data.pageId, 'pageId', 255)
|
||||
if (!validation.isValid) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: validation.error || 'Invalid page ID',
|
||||
path: ['pageId'],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const confluencePageBaseSchema = z.object({
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: optionalString,
|
||||
pageId: z.string().min(1, 'Page ID is required'),
|
||||
})
|
||||
|
||||
export const confluencePageBodySchema = confluencePageBaseSchema.superRefine(refineConfluencePageId)
|
||||
|
||||
/** Body schema for `PUT /api/tools/confluence/page`. */
|
||||
export const confluenceUpdatePageBodySchema = confluencePageBaseSchema
|
||||
.extend({
|
||||
title: optionalString,
|
||||
body: z.object({ value: optionalString }).optional(),
|
||||
version: z.object({ message: optionalString }).optional(),
|
||||
})
|
||||
.superRefine(refineConfluencePageId)
|
||||
|
||||
/** Body schema for `DELETE /api/tools/confluence/page`. */
|
||||
export const confluenceDeletePageBodySchema = confluencePageBaseSchema
|
||||
.extend({
|
||||
purge: z.boolean().optional(),
|
||||
})
|
||||
.superRefine(refineConfluencePageId)
|
||||
|
||||
const confluenceBaseSchema = z.object({
|
||||
domain: z.string({ error: 'Domain is required' }).min(1, 'Domain is required'),
|
||||
accessToken: z.string({ error: 'Access token is required' }).min(1, 'Access token is required'),
|
||||
cloudId: z.string().optional(),
|
||||
})
|
||||
|
||||
const confluencePageScopedSchema = confluenceBaseSchema.extend({
|
||||
pageId: z.string({ error: 'Page ID is required' }).min(1, 'Page ID is required'),
|
||||
})
|
||||
|
||||
export const confluenceSpaceScopedSchema = confluenceBaseSchema.extend({
|
||||
spaceId: z.string({ error: 'Space ID is required' }).min(1, 'Space ID is required'),
|
||||
})
|
||||
|
||||
function addAlphanumericIdIssue(
|
||||
data: Record<string, unknown>,
|
||||
field: string,
|
||||
label: string,
|
||||
ctx: z.RefinementCtx
|
||||
): void {
|
||||
const value = data[field]
|
||||
if (typeof value !== 'string') return
|
||||
|
||||
const validation = validateAlphanumericId(value, field, 255)
|
||||
if (!validation.isValid) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: validation.error || `Invalid ${label}`,
|
||||
path: [field],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the un-superRefined base separate so downstream schemas can .extend it.
|
||||
// .superRefine returns a ZodEffects which has no .extend method, so extending
|
||||
// the refined schema directly throws at module-init time (caught by bundlers
|
||||
// like esbuild/Trigger.dev that eagerly evaluate; Next.js lazy-loads per-route
|
||||
// and hides the issue).
|
||||
const confluenceCommentScopedBaseSchema = confluenceBaseSchema.extend({
|
||||
commentId: z.string().min(1, 'Comment ID is required'),
|
||||
})
|
||||
export const confluenceCommentScopedSchema = confluenceCommentScopedBaseSchema.superRefine(
|
||||
(data, ctx) => addAlphanumericIdIssue(data, 'commentId', 'comment ID', ctx)
|
||||
)
|
||||
|
||||
const confluenceBlogPostScopedBaseSchema = confluenceBaseSchema.extend({
|
||||
blogPostId: z.string({ error: 'Blog post ID is required' }).min(1, 'Blog post ID is required'),
|
||||
})
|
||||
export const confluenceBlogPostScopedSchema = confluenceBlogPostScopedBaseSchema.superRefine(
|
||||
(data, ctx) => addAlphanumericIdIssue(data, 'blogPostId', 'blog post ID', ctx)
|
||||
)
|
||||
|
||||
export const confluenceDeleteAttachmentBodySchema = confluenceBaseSchema.extend({
|
||||
attachmentId: z
|
||||
.string({ error: 'Attachment ID is required' })
|
||||
.min(1, 'Attachment ID is required'),
|
||||
})
|
||||
|
||||
export const confluenceListAttachmentsQuerySchema = confluencePageScopedSchema.extend({
|
||||
limit: z.string().optional().default('50'),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceCreateCommentBodySchema = confluencePageScopedSchema.extend({
|
||||
comment: z.string({ error: 'Comment is required' }).min(1, 'Comment is required'),
|
||||
})
|
||||
|
||||
export const confluenceListCommentsQuerySchema = confluencePageScopedSchema.extend({
|
||||
limit: z.string().optional().default('25'),
|
||||
bodyFormat: z.string().optional().default('storage'),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceUpdateCommentBodySchema = confluenceCommentScopedBaseSchema
|
||||
.extend({
|
||||
comment: z.string().min(1, 'Comment is required'),
|
||||
})
|
||||
.superRefine((data, ctx) => addAlphanumericIdIssue(data, 'commentId', 'comment ID', ctx))
|
||||
|
||||
export const confluenceCreatePageBodySchema = confluenceSpaceScopedSchema.extend({
|
||||
title: z.string({ error: 'Title is required' }).min(1, 'Title is required'),
|
||||
content: z.string({ error: 'Content is required' }).min(1, 'Content is required'),
|
||||
parentId: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceLabelMutationBodySchema = confluencePageScopedSchema.extend({
|
||||
labelName: z.string({ error: 'Label name is required' }).min(1, 'Label name is required'),
|
||||
prefix: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceListLabelsQuerySchema = confluencePageScopedSchema.extend({
|
||||
limit: z.string().optional().default('25'),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceListPagePropertiesQuerySchema = confluencePageScopedSchema.extend({
|
||||
limit: z.string().optional().default('50'),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceCreatePagePropertyBodySchema = confluencePageScopedSchema.extend({
|
||||
key: z.string({ error: 'Property key is required' }).min(1, 'Property key is required'),
|
||||
value: z.unknown(),
|
||||
})
|
||||
|
||||
export const confluenceUpdatePagePropertyBodySchema = confluenceCreatePagePropertyBodySchema.extend(
|
||||
{
|
||||
propertyId: z.string({ error: 'Property ID is required' }).min(1, 'Property ID is required'),
|
||||
versionNumber: z.number().min(1).optional(),
|
||||
}
|
||||
)
|
||||
|
||||
export const confluenceDeletePagePropertyBodySchema = confluencePageScopedSchema.extend({
|
||||
propertyId: z.string({ error: 'Property ID is required' }).min(1, 'Property ID is required'),
|
||||
})
|
||||
|
||||
export const confluenceGetSpaceQuerySchema = confluenceSpaceScopedSchema
|
||||
export const confluenceCreateSpaceBodySchema = confluenceBaseSchema.extend({
|
||||
name: z.string({ error: 'Space name is required' }).min(1, 'Space name is required'),
|
||||
key: z.string({ error: 'Space key is required' }).min(1, 'Space key is required'),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
export const confluenceUpdateSpaceBodySchema = confluenceSpaceScopedSchema.extend({
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
export const confluencePageChildrenBodySchema = confluencePageScopedSchema.extend({
|
||||
limit: z.number().optional().default(50),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluencePageAncestorsBodySchema = confluencePageScopedSchema.extend({
|
||||
limit: z.number().optional().default(25),
|
||||
})
|
||||
|
||||
export const confluencePageVersionsBodySchema = confluencePageScopedSchema.extend({
|
||||
versionNumber: z.union([z.string(), z.number()]).optional(),
|
||||
limit: z.number().optional().default(50),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluencePagesByLabelQuerySchema = confluenceBaseSchema.extend({
|
||||
labelId: z.string({ error: 'Label ID is required' }).min(1, 'Label ID is required'),
|
||||
limit: z.string().optional().default('50'),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceSearchBodySchema = confluenceBaseSchema.extend({
|
||||
query: z.string({ error: 'Search query is required' }).min(1, 'Search query is required'),
|
||||
limit: z.number().optional().default(25),
|
||||
})
|
||||
|
||||
export const confluenceSearchInSpaceBodySchema = confluenceBaseSchema.extend({
|
||||
spaceKey: z.string({ error: 'Space key is required' }).min(1, 'Space key is required'),
|
||||
query: z.string().optional(),
|
||||
limit: z.number().optional().default(25),
|
||||
contentType: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceSpaceBlogPostsBodySchema = confluenceSpaceScopedSchema.extend({
|
||||
limit: z.number().optional().default(25),
|
||||
status: z.string().optional(),
|
||||
bodyFormat: z.string().optional(),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceSpaceLabelsQuerySchema = confluenceSpaceScopedSchema.extend({
|
||||
limit: z.string().optional().default('25'),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceSpacePagesBodySchema = confluenceSpaceScopedSchema.extend({
|
||||
limit: z.number().optional().default(50),
|
||||
status: z.string().optional(),
|
||||
bodyFormat: z.string().optional(),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceSpacePermissionsBodySchema = confluenceSpaceScopedSchema.extend({
|
||||
limit: z.number().optional().default(50),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceSpacePropertiesBodySchema = confluenceSpaceScopedSchema.extend({
|
||||
action: z.string().optional(),
|
||||
key: z.string().optional(),
|
||||
value: z.unknown().optional(),
|
||||
propertyId: z.string().optional(),
|
||||
limit: z.number().optional().default(50),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceListSpacesQuerySchema = confluenceBaseSchema.extend({
|
||||
limit: z.string().optional().default('25'),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceTasksBodySchema = confluenceBaseSchema.extend({
|
||||
action: z.string().optional(),
|
||||
taskId: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
pageId: z.string().optional(),
|
||||
spaceId: z.string().optional(),
|
||||
assignedTo: z.string().optional(),
|
||||
limit: z.number().optional().default(50),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceUploadAttachmentBodySchema = confluencePageScopedSchema.extend({
|
||||
file: z.unknown().refine((value) => Boolean(value), { message: 'File is required' }),
|
||||
fileName: z.string().optional(),
|
||||
comment: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceUserBodySchema = confluenceBaseSchema.extend({
|
||||
accountId: z.string({ error: 'Account ID is required' }).min(1, 'Account ID is required'),
|
||||
})
|
||||
|
||||
export const confluenceGetBlogPostBodySchema = confluenceBlogPostScopedBaseSchema
|
||||
.extend({
|
||||
bodyFormat: z.string().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => addAlphanumericIdIssue(data, 'blogPostId', 'blog post ID', ctx))
|
||||
|
||||
export const confluenceCreateBlogPostBodySchema = confluenceSpaceScopedSchema.extend({
|
||||
title: z.string({ error: 'Title is required' }).min(1, 'Title is required'),
|
||||
content: z.string({ error: 'Content is required' }).min(1, 'Content is required'),
|
||||
status: z.enum(['current', 'draft']).optional(),
|
||||
})
|
||||
|
||||
export const confluenceBlogPostOperationBodySchema = z.union([
|
||||
confluenceCreateBlogPostBodySchema,
|
||||
confluenceGetBlogPostBodySchema,
|
||||
])
|
||||
|
||||
export const confluenceListBlogPostsQuerySchema = confluenceBaseSchema.extend({
|
||||
limit: z.string().optional().default('25'),
|
||||
status: z.string().optional(),
|
||||
sort: z.string().optional(),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confluenceUpdateBlogPostBodySchema = confluenceBlogPostScopedBaseSchema
|
||||
.extend({
|
||||
title: z.string().optional(),
|
||||
content: z.string().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => addAlphanumericIdIssue(data, 'blogPostId', 'blog post ID', ctx))
|
||||
|
||||
const defineConfluencePostContract = <TBody extends z.ZodType>(path: string, body: TBody) =>
|
||||
defineRouteContract({
|
||||
method: 'POST',
|
||||
path,
|
||||
body,
|
||||
response: {
|
||||
mode: 'json',
|
||||
// untyped-response: shared helper for ~16 confluence POST routes, each forwarding a different Atlassian Confluence v2 payload (pages, comments, labels, blogposts, page properties, search, etc.) whose shapes diverge per resource and are version-dependent
|
||||
schema: z.unknown(),
|
||||
},
|
||||
})
|
||||
|
||||
const defineConfluencePutContract = <TBody extends z.ZodType>(path: string, body: TBody) =>
|
||||
defineRouteContract({
|
||||
method: 'PUT',
|
||||
path,
|
||||
body,
|
||||
response: {
|
||||
mode: 'json',
|
||||
// untyped-response: shared helper for confluence PUT routes (page, comment, blogpost, space, page-properties) that proxy raw Atlassian Confluence v2 update responses whose shape varies per resource
|
||||
schema: z.unknown(),
|
||||
},
|
||||
})
|
||||
|
||||
const defineConfluenceDeleteContract = <TBody extends z.ZodType>(path: string, body: TBody) =>
|
||||
defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path,
|
||||
body,
|
||||
response: {
|
||||
mode: 'json',
|
||||
// untyped-response: shared helper for confluence DELETE routes returning either a normalized deleted marker, an empty body, or a forwarded Atlassian Confluence v2 response depending on the resource
|
||||
schema: z.unknown(),
|
||||
},
|
||||
})
|
||||
|
||||
const defineConfluenceGetContract = <TQuery extends z.ZodType>(path: string, query: TQuery) =>
|
||||
defineRouteContract({
|
||||
method: 'GET',
|
||||
path,
|
||||
query,
|
||||
response: {
|
||||
mode: 'json',
|
||||
// untyped-response: shared helper for confluence GET listing routes (attachments, blogposts, comments, labels, page-properties, space, spaces, space-labels, pages-by-label) each returning a different paginated Atlassian Confluence v2 shape
|
||||
schema: z.unknown(),
|
||||
},
|
||||
})
|
||||
|
||||
export const confluenceSpacesSelectorBodySchema = credentialWorkflowDomainBodySchema.extend({
|
||||
cursor: optionalString,
|
||||
})
|
||||
|
||||
export const confluenceSpacesSelectorContract = definePostSelector(
|
||||
'/api/tools/confluence/selector-spaces',
|
||||
confluenceSpacesSelectorBodySchema,
|
||||
z.object({
|
||||
spaces: z.array(confluenceSpaceSchema),
|
||||
nextCursor: optionalString,
|
||||
})
|
||||
)
|
||||
|
||||
export const confluencePagesSelectorContract = definePostSelector(
|
||||
'/api/tools/confluence/pages',
|
||||
confluencePagesBodySchema,
|
||||
z.object({ files: z.array(fileOptionSchema) })
|
||||
)
|
||||
|
||||
export const confluencePageSelectorContract = definePostSelector(
|
||||
'/api/tools/confluence/page',
|
||||
confluencePageBodySchema,
|
||||
z.object({ id: z.string(), title: z.string() }).passthrough()
|
||||
)
|
||||
|
||||
export const confluenceUpdatePageContract = defineConfluencePutContract(
|
||||
'/api/tools/confluence/page',
|
||||
confluenceUpdatePageBodySchema
|
||||
)
|
||||
export const confluenceDeletePageContract = defineConfluenceDeleteContract(
|
||||
'/api/tools/confluence/page',
|
||||
confluenceDeletePageBodySchema
|
||||
)
|
||||
export const confluenceDeleteAttachmentContract = defineConfluenceDeleteContract(
|
||||
'/api/tools/confluence/attachment',
|
||||
confluenceDeleteAttachmentBodySchema
|
||||
)
|
||||
export const confluenceListAttachmentsContract = defineConfluenceGetContract(
|
||||
'/api/tools/confluence/attachments',
|
||||
confluenceListAttachmentsQuerySchema
|
||||
)
|
||||
export const confluenceListBlogPostsContract = defineConfluenceGetContract(
|
||||
'/api/tools/confluence/blogposts',
|
||||
confluenceListBlogPostsQuerySchema
|
||||
)
|
||||
export const confluenceBlogPostOperationContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/blogposts',
|
||||
confluenceBlogPostOperationBodySchema
|
||||
)
|
||||
export const confluenceUpdateBlogPostContract = defineConfluencePutContract(
|
||||
'/api/tools/confluence/blogposts',
|
||||
confluenceUpdateBlogPostBodySchema
|
||||
)
|
||||
export const confluenceDeleteBlogPostContract = defineConfluenceDeleteContract(
|
||||
'/api/tools/confluence/blogposts',
|
||||
confluenceBlogPostScopedSchema
|
||||
)
|
||||
export const confluenceCreateCommentContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/comments',
|
||||
confluenceCreateCommentBodySchema
|
||||
)
|
||||
export const confluenceListCommentsContract = defineConfluenceGetContract(
|
||||
'/api/tools/confluence/comments',
|
||||
confluenceListCommentsQuerySchema
|
||||
)
|
||||
export const confluenceUpdateCommentContract = defineConfluencePutContract(
|
||||
'/api/tools/confluence/comment',
|
||||
confluenceUpdateCommentBodySchema
|
||||
)
|
||||
export const confluenceDeleteCommentContract = defineConfluenceDeleteContract(
|
||||
'/api/tools/confluence/comment',
|
||||
confluenceCommentScopedSchema
|
||||
)
|
||||
export const confluenceCreatePageContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/create-page',
|
||||
confluenceCreatePageBodySchema
|
||||
)
|
||||
export const confluenceLabelMutationContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/labels',
|
||||
confluenceLabelMutationBodySchema
|
||||
)
|
||||
export const confluenceListLabelsContract = defineConfluenceGetContract(
|
||||
'/api/tools/confluence/labels',
|
||||
confluenceListLabelsQuerySchema
|
||||
)
|
||||
export const confluenceDeleteLabelContract = defineConfluenceDeleteContract(
|
||||
'/api/tools/confluence/labels',
|
||||
confluenceLabelMutationBodySchema
|
||||
)
|
||||
export const confluenceListPagePropertiesContract = defineConfluenceGetContract(
|
||||
'/api/tools/confluence/page-properties',
|
||||
confluenceListPagePropertiesQuerySchema
|
||||
)
|
||||
export const confluenceCreatePagePropertyContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/page-properties',
|
||||
confluenceCreatePagePropertyBodySchema
|
||||
)
|
||||
export const confluenceUpdatePagePropertyContract = defineConfluencePutContract(
|
||||
'/api/tools/confluence/page-properties',
|
||||
confluenceUpdatePagePropertyBodySchema
|
||||
)
|
||||
export const confluenceDeletePagePropertyContract = defineConfluenceDeleteContract(
|
||||
'/api/tools/confluence/page-properties',
|
||||
confluenceDeletePagePropertyBodySchema
|
||||
)
|
||||
export const confluenceGetSpaceContract = defineConfluenceGetContract(
|
||||
'/api/tools/confluence/space',
|
||||
confluenceGetSpaceQuerySchema
|
||||
)
|
||||
export const confluenceCreateSpaceContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/space',
|
||||
confluenceCreateSpaceBodySchema
|
||||
)
|
||||
export const confluenceUpdateSpaceContract = defineConfluencePutContract(
|
||||
'/api/tools/confluence/space',
|
||||
confluenceUpdateSpaceBodySchema
|
||||
)
|
||||
export const confluenceDeleteSpaceContract = defineConfluenceDeleteContract(
|
||||
'/api/tools/confluence/space',
|
||||
confluenceSpaceScopedSchema
|
||||
)
|
||||
export const confluencePageChildrenContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/page-children',
|
||||
confluencePageChildrenBodySchema
|
||||
)
|
||||
export const confluencePageDescendantsContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/page-descendants',
|
||||
confluencePageChildrenBodySchema
|
||||
)
|
||||
export const confluencePageAncestorsContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/page-ancestors',
|
||||
confluencePageAncestorsBodySchema
|
||||
)
|
||||
export const confluencePageVersionsContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/page-versions',
|
||||
confluencePageVersionsBodySchema
|
||||
)
|
||||
export const confluencePagesByLabelContract = defineConfluenceGetContract(
|
||||
'/api/tools/confluence/pages-by-label',
|
||||
confluencePagesByLabelQuerySchema
|
||||
)
|
||||
export const confluenceSearchContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/search',
|
||||
confluenceSearchBodySchema
|
||||
)
|
||||
export const confluenceSearchInSpaceContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/search-in-space',
|
||||
confluenceSearchInSpaceBodySchema
|
||||
)
|
||||
export const confluenceSpaceBlogPostsContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/space-blogposts',
|
||||
confluenceSpaceBlogPostsBodySchema
|
||||
)
|
||||
export const confluenceSpaceLabelsContract = defineConfluenceGetContract(
|
||||
'/api/tools/confluence/space-labels',
|
||||
confluenceSpaceLabelsQuerySchema
|
||||
)
|
||||
export const confluenceSpacePagesContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/space-pages',
|
||||
confluenceSpacePagesBodySchema
|
||||
)
|
||||
export const confluenceSpacePermissionsContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/space-permissions',
|
||||
confluenceSpacePermissionsBodySchema
|
||||
)
|
||||
export const confluenceSpacePropertiesContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/space-properties',
|
||||
confluenceSpacePropertiesBodySchema
|
||||
)
|
||||
export const confluenceListSpacesContract = defineConfluenceGetContract(
|
||||
'/api/tools/confluence/spaces',
|
||||
confluenceListSpacesQuerySchema
|
||||
)
|
||||
export const confluenceTasksContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/tasks',
|
||||
confluenceTasksBodySchema
|
||||
)
|
||||
export const confluenceUploadAttachmentContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/upload-attachment',
|
||||
confluenceUploadAttachmentBodySchema
|
||||
)
|
||||
export const confluenceUserContract = defineConfluencePostContract(
|
||||
'/api/tools/confluence/user',
|
||||
confluenceUserBodySchema
|
||||
)
|
||||
|
||||
export type ConfluencePagesBody = ContractBody<typeof confluencePagesSelectorContract>
|
||||
export type ConfluencePageBody = ContractBody<typeof confluencePageSelectorContract>
|
||||
export type ConfluenceUpdatePageBody = ContractBody<typeof confluenceUpdatePageContract>
|
||||
export type ConfluenceDeletePageBody = ContractBody<typeof confluenceDeletePageContract>
|
||||
export type ConfluenceDeleteAttachmentBody = ContractBody<typeof confluenceDeleteAttachmentContract>
|
||||
export type ConfluenceListAttachmentsQuery = ContractQuery<typeof confluenceListAttachmentsContract>
|
||||
export type ConfluenceListBlogPostsQuery = ContractQuery<typeof confluenceListBlogPostsContract>
|
||||
export type ConfluenceBlogPostOperationBody = ContractBody<
|
||||
typeof confluenceBlogPostOperationContract
|
||||
>
|
||||
export type ConfluenceUpdateBlogPostBody = ContractBody<typeof confluenceUpdateBlogPostContract>
|
||||
export type ConfluenceDeleteBlogPostBody = ContractBody<typeof confluenceDeleteBlogPostContract>
|
||||
export type ConfluenceCreateCommentBody = ContractBody<typeof confluenceCreateCommentContract>
|
||||
export type ConfluenceListCommentsQuery = ContractQuery<typeof confluenceListCommentsContract>
|
||||
export type ConfluenceUpdateCommentBody = ContractBody<typeof confluenceUpdateCommentContract>
|
||||
export type ConfluenceDeleteCommentBody = ContractBody<typeof confluenceDeleteCommentContract>
|
||||
export type ConfluenceCreatePageBody = ContractBody<typeof confluenceCreatePageContract>
|
||||
export type ConfluenceLabelMutationBody = ContractBody<typeof confluenceLabelMutationContract>
|
||||
export type ConfluenceListLabelsQuery = ContractQuery<typeof confluenceListLabelsContract>
|
||||
export type ConfluenceDeleteLabelBody = ContractBody<typeof confluenceDeleteLabelContract>
|
||||
export type ConfluenceListPagePropertiesQuery = ContractQuery<
|
||||
typeof confluenceListPagePropertiesContract
|
||||
>
|
||||
export type ConfluenceCreatePagePropertyBody = ContractBody<
|
||||
typeof confluenceCreatePagePropertyContract
|
||||
>
|
||||
export type ConfluenceUpdatePagePropertyBody = ContractBody<
|
||||
typeof confluenceUpdatePagePropertyContract
|
||||
>
|
||||
export type ConfluenceDeletePagePropertyBody = ContractBody<
|
||||
typeof confluenceDeletePagePropertyContract
|
||||
>
|
||||
export type ConfluenceGetSpaceQuery = ContractQuery<typeof confluenceGetSpaceContract>
|
||||
export type ConfluenceCreateSpaceBody = ContractBody<typeof confluenceCreateSpaceContract>
|
||||
export type ConfluenceUpdateSpaceBody = ContractBody<typeof confluenceUpdateSpaceContract>
|
||||
export type ConfluenceDeleteSpaceBody = ContractBody<typeof confluenceDeleteSpaceContract>
|
||||
export type ConfluencePageChildrenBody = ContractBody<typeof confluencePageChildrenContract>
|
||||
export type ConfluencePageDescendantsBody = ContractBody<typeof confluencePageDescendantsContract>
|
||||
export type ConfluencePageAncestorsBody = ContractBody<typeof confluencePageAncestorsContract>
|
||||
export type ConfluencePageVersionsBody = ContractBody<typeof confluencePageVersionsContract>
|
||||
export type ConfluencePagesByLabelQuery = ContractQuery<typeof confluencePagesByLabelContract>
|
||||
export type ConfluenceSearchBody = ContractBody<typeof confluenceSearchContract>
|
||||
export type ConfluenceSearchInSpaceBody = ContractBody<typeof confluenceSearchInSpaceContract>
|
||||
export type ConfluenceSpaceBlogPostsBody = ContractBody<typeof confluenceSpaceBlogPostsContract>
|
||||
export type ConfluenceSpaceLabelsQuery = ContractQuery<typeof confluenceSpaceLabelsContract>
|
||||
export type ConfluenceSpacePagesBody = ContractBody<typeof confluenceSpacePagesContract>
|
||||
export type ConfluenceSpacePermissionsBody = ContractBody<typeof confluenceSpacePermissionsContract>
|
||||
export type ConfluenceSpacePropertiesBody = ContractBody<typeof confluenceSpacePropertiesContract>
|
||||
export type ConfluenceListSpacesQuery = ContractQuery<typeof confluenceListSpacesContract>
|
||||
export type ConfluenceTasksBody = ContractBody<typeof confluenceTasksContract>
|
||||
export type ConfluenceUploadAttachmentBody = ContractBody<typeof confluenceUploadAttachmentContract>
|
||||
export type ConfluenceUserBody = ContractBody<typeof confluenceUserContract>
|
||||
export type ConfluenceSpacesSelectorResponse = ContractJsonResponse<
|
||||
typeof confluenceSpacesSelectorContract
|
||||
>
|
||||
export type ConfluencePagesSelectorResponse = ContractJsonResponse<
|
||||
typeof confluencePagesSelectorContract
|
||||
>
|
||||
export type ConfluencePageSelectorResponse = ContractJsonResponse<
|
||||
typeof confluencePageSelectorContract
|
||||
>
|
||||
@@ -0,0 +1,136 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialIdQuerySchema,
|
||||
credentialWorkflowImpersonateBodySchema,
|
||||
defineGetSelector,
|
||||
definePostSelector,
|
||||
fileOptionSchema,
|
||||
folderOptionSchema,
|
||||
idNameSchema,
|
||||
idTitleSchema,
|
||||
optionalString,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type {
|
||||
ContractBodyInput,
|
||||
ContractJsonResponse,
|
||||
ContractQueryInput,
|
||||
} from '@/lib/api/contracts/types'
|
||||
|
||||
const googleCalendarSchema = z.object({ id: z.string(), summary: z.string() }).passthrough()
|
||||
const gmailLabelSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string().optional(),
|
||||
messagesTotal: z.number().optional(),
|
||||
messagesUnread: z.number().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const labelsQuerySchema = credentialIdQuerySchema.extend({
|
||||
query: optionalString,
|
||||
impersonateEmail: optionalString,
|
||||
})
|
||||
|
||||
export const gmailLabelQuerySchema = credentialIdQuerySchema.extend({
|
||||
labelId: z.string().min(1),
|
||||
impersonateEmail: optionalString,
|
||||
})
|
||||
|
||||
export const googleCalendarQuerySchema = credentialIdQuerySchema.extend({
|
||||
workflowId: optionalString,
|
||||
impersonateEmail: optionalString,
|
||||
})
|
||||
|
||||
export const googleDriveFilesQuerySchema = credentialIdQuerySchema.extend({
|
||||
mimeType: optionalString,
|
||||
folderId: optionalString,
|
||||
parentId: optionalString,
|
||||
query: optionalString,
|
||||
workflowId: optionalString,
|
||||
impersonateEmail: optionalString,
|
||||
})
|
||||
|
||||
export const googleDriveFileQuerySchema = credentialIdQuerySchema.extend({
|
||||
fileId: z.string().min(1, 'File ID is required'),
|
||||
workflowId: optionalString,
|
||||
impersonateEmail: optionalString,
|
||||
})
|
||||
|
||||
export const googleSheetsQuerySchema = credentialIdQuerySchema.extend({
|
||||
spreadsheetId: z.string().min(1, 'Spreadsheet ID is required'),
|
||||
workflowId: optionalString,
|
||||
impersonateEmail: optionalString,
|
||||
})
|
||||
|
||||
export const gmailLabelsSelectorContract = defineGetSelector(
|
||||
'/api/tools/gmail/labels',
|
||||
labelsQuerySchema,
|
||||
z.object({ labels: z.array(folderOptionSchema) })
|
||||
)
|
||||
|
||||
export const gmailLabelSelectorContract = defineGetSelector(
|
||||
'/api/tools/gmail/label',
|
||||
gmailLabelQuerySchema,
|
||||
z.object({ label: gmailLabelSchema })
|
||||
)
|
||||
|
||||
export const googleCalendarSelectorContract = defineGetSelector(
|
||||
'/api/tools/google_calendar/calendars',
|
||||
googleCalendarQuerySchema,
|
||||
z.object({ calendars: z.array(googleCalendarSchema) })
|
||||
)
|
||||
|
||||
export const googleTasksTaskListsSelectorContract = definePostSelector(
|
||||
'/api/tools/google_tasks/task-lists',
|
||||
credentialWorkflowImpersonateBodySchema,
|
||||
z.object({ taskLists: z.array(idTitleSchema) })
|
||||
)
|
||||
|
||||
export const googleDriveFilesSelectorContract = defineGetSelector(
|
||||
'/api/tools/drive/files',
|
||||
googleDriveFilesQuerySchema,
|
||||
z.object({ files: z.array(fileOptionSchema) })
|
||||
)
|
||||
|
||||
export const googleDriveFileSelectorContract = defineGetSelector(
|
||||
'/api/tools/drive/file',
|
||||
googleDriveFileQuerySchema,
|
||||
z.object({ file: fileOptionSchema.optional() }).passthrough()
|
||||
)
|
||||
|
||||
export const googleSheetsSelectorContract = defineGetSelector(
|
||||
'/api/tools/google_sheets/sheets',
|
||||
googleSheetsQuerySchema,
|
||||
z.object({ sheets: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
export type GmailLabelsSelectorQuery = ContractQueryInput<typeof gmailLabelsSelectorContract>
|
||||
export type GmailLabelSelectorQuery = ContractQueryInput<typeof gmailLabelSelectorContract>
|
||||
export type GoogleCalendarSelectorQuery = ContractQueryInput<typeof googleCalendarSelectorContract>
|
||||
export type GoogleTasksTaskListsSelectorBody = ContractBodyInput<
|
||||
typeof googleTasksTaskListsSelectorContract
|
||||
>
|
||||
export type GoogleDriveFilesSelectorQuery = ContractQueryInput<
|
||||
typeof googleDriveFilesSelectorContract
|
||||
>
|
||||
export type GoogleDriveFileSelectorQuery = ContractQueryInput<
|
||||
typeof googleDriveFileSelectorContract
|
||||
>
|
||||
export type GoogleSheetsSelectorQuery = ContractQueryInput<typeof googleSheetsSelectorContract>
|
||||
|
||||
export type GmailLabelsSelectorResponse = ContractJsonResponse<typeof gmailLabelsSelectorContract>
|
||||
export type GmailLabelSelectorResponse = ContractJsonResponse<typeof gmailLabelSelectorContract>
|
||||
export type GoogleCalendarSelectorResponse = ContractJsonResponse<
|
||||
typeof googleCalendarSelectorContract
|
||||
>
|
||||
export type GoogleTasksTaskListsSelectorResponse = ContractJsonResponse<
|
||||
typeof googleTasksTaskListsSelectorContract
|
||||
>
|
||||
export type GoogleDriveFilesSelectorResponse = ContractJsonResponse<
|
||||
typeof googleDriveFilesSelectorContract
|
||||
>
|
||||
export type GoogleDriveFileSelectorResponse = ContractJsonResponse<
|
||||
typeof googleDriveFileSelectorContract
|
||||
>
|
||||
export type GoogleSheetsSelectorResponse = ContractJsonResponse<typeof googleSheetsSelectorContract>
|
||||
@@ -0,0 +1,110 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialIdQuerySchema,
|
||||
defineGetSelector,
|
||||
optionalString,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractJsonResponse, ContractQueryInput } from '@/lib/api/contracts/types'
|
||||
|
||||
const hubspotPropertySchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string().optional(),
|
||||
fieldType: z.string().optional(),
|
||||
groupName: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const hubspotListSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
objectType: z.string().optional(),
|
||||
processingType: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const hubspotPipelineSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
stages: z.array(z.object({ id: z.string(), label: z.string() }).passthrough()).optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const hubspotOwnerSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const hubspotPropertiesQuerySchema = credentialIdQuerySchema.extend({
|
||||
objectType: z
|
||||
.string()
|
||||
.min(1, 'objectType is required')
|
||||
.describe('Built-in slug or custom object type id'),
|
||||
query: optionalString,
|
||||
})
|
||||
|
||||
const hubspotListsQuerySchema = credentialIdQuerySchema.extend({
|
||||
objectTypeId: optionalString.describe('Limit to lists targeting this object type'),
|
||||
query: optionalString,
|
||||
})
|
||||
|
||||
const hubspotPipelinesQuerySchema = credentialIdQuerySchema.extend({
|
||||
objectType: z
|
||||
.string()
|
||||
.min(1, 'objectType is required')
|
||||
.describe("Object type for which to fetch pipelines (e.g., 'deal' or 'ticket')"),
|
||||
})
|
||||
|
||||
const hubspotOwnersQuerySchema = credentialIdQuerySchema.extend({
|
||||
query: optionalString,
|
||||
})
|
||||
|
||||
export const hubspotPropertiesSelectorContract = defineGetSelector(
|
||||
'/api/tools/hubspot/properties',
|
||||
hubspotPropertiesQuerySchema,
|
||||
z.object({ properties: z.array(hubspotPropertySchema) })
|
||||
)
|
||||
|
||||
export const hubspotListsSelectorContract = defineGetSelector(
|
||||
'/api/tools/hubspot/lists',
|
||||
hubspotListsQuerySchema,
|
||||
z.object({ lists: z.array(hubspotListSchema) })
|
||||
)
|
||||
|
||||
export const hubspotPipelinesSelectorContract = defineGetSelector(
|
||||
'/api/tools/hubspot/pipelines',
|
||||
hubspotPipelinesQuerySchema,
|
||||
z.object({ pipelines: z.array(hubspotPipelineSchema) })
|
||||
)
|
||||
|
||||
export const hubspotOwnersSelectorContract = defineGetSelector(
|
||||
'/api/tools/hubspot/owners',
|
||||
hubspotOwnersQuerySchema,
|
||||
z.object({ owners: z.array(hubspotOwnerSchema) })
|
||||
)
|
||||
|
||||
export type HubspotPropertiesSelectorQuery = ContractQueryInput<
|
||||
typeof hubspotPropertiesSelectorContract
|
||||
>
|
||||
export type HubspotListsSelectorQuery = ContractQueryInput<typeof hubspotListsSelectorContract>
|
||||
export type HubspotPipelinesSelectorQuery = ContractQueryInput<
|
||||
typeof hubspotPipelinesSelectorContract
|
||||
>
|
||||
export type HubspotOwnersSelectorQuery = ContractQueryInput<typeof hubspotOwnersSelectorContract>
|
||||
|
||||
export type HubspotPropertiesSelectorResponse = ContractJsonResponse<
|
||||
typeof hubspotPropertiesSelectorContract
|
||||
>
|
||||
export type HubspotListsSelectorResponse = ContractJsonResponse<typeof hubspotListsSelectorContract>
|
||||
export type HubspotPipelinesSelectorResponse = ContractJsonResponse<
|
||||
typeof hubspotPipelinesSelectorContract
|
||||
>
|
||||
export type HubspotOwnersSelectorResponse = ContractJsonResponse<
|
||||
typeof hubspotOwnersSelectorContract
|
||||
>
|
||||
@@ -0,0 +1,200 @@
|
||||
import {
|
||||
airtableBasesSelectorContract,
|
||||
airtableTablesSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/airtable'
|
||||
import { asanaWorkspacesSelectorContract } from '@/lib/api/contracts/selectors/asana'
|
||||
import {
|
||||
attioListsSelectorContract,
|
||||
attioObjectsSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/attio'
|
||||
import {
|
||||
bigQueryDatasetsSelectorContract,
|
||||
bigQueryTablesSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/bigquery'
|
||||
import {
|
||||
calcomEventTypesSelectorContract,
|
||||
calcomSchedulesSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/calcom'
|
||||
import {
|
||||
cloudwatchLogGroupsSelectorContract,
|
||||
cloudwatchLogStreamsSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/cloudwatch'
|
||||
import {
|
||||
confluencePageSelectorContract,
|
||||
confluencePagesSelectorContract,
|
||||
confluenceSpacesSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/confluence'
|
||||
import {
|
||||
gmailLabelSelectorContract,
|
||||
gmailLabelsSelectorContract,
|
||||
googleCalendarSelectorContract,
|
||||
googleDriveFileSelectorContract,
|
||||
googleDriveFilesSelectorContract,
|
||||
googleSheetsSelectorContract,
|
||||
googleTasksTaskListsSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/google'
|
||||
import {
|
||||
hubspotListsSelectorContract,
|
||||
hubspotOwnersSelectorContract,
|
||||
hubspotPipelinesSelectorContract,
|
||||
hubspotPropertiesSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/hubspot'
|
||||
import {
|
||||
jiraIssueSelectorContract,
|
||||
jiraIssuesSelectorContract,
|
||||
jiraProjectSelectorContract,
|
||||
jiraProjectsSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/jira'
|
||||
import {
|
||||
jsmRequestTypesSelectorContract,
|
||||
jsmServiceDesksSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/jsm'
|
||||
import {
|
||||
linearProjectsSelectorContract,
|
||||
linearTeamsSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/linear'
|
||||
import {
|
||||
microsoftChannelsSelectorContract,
|
||||
microsoftChatsSelectorContract,
|
||||
microsoftExcelDriveSelectorContract,
|
||||
microsoftExcelDrivesSelectorContract,
|
||||
microsoftExcelSheetsSelectorContract,
|
||||
microsoftFileSelectorContract,
|
||||
microsoftFilesSelectorContract,
|
||||
microsoftPlannerPlansSelectorContract,
|
||||
microsoftPlannerTasksSelectorContract,
|
||||
microsoftTeamsSelectorContract,
|
||||
onedriveFilesSelectorContract,
|
||||
onedriveFolderSelectorContract,
|
||||
onedriveFoldersSelectorContract,
|
||||
outlookFoldersSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/microsoft'
|
||||
import {
|
||||
mondayBoardsSelectorContract,
|
||||
mondayGroupsSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/monday'
|
||||
import {
|
||||
notionDatabasesSelectorContract,
|
||||
notionPagesSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/notion'
|
||||
import { pipedrivePipelinesSelectorContract } from '@/lib/api/contracts/selectors/pipedrive'
|
||||
import {
|
||||
sharepointListsSelectorContract,
|
||||
sharepointSiteSelectorContract,
|
||||
sharepointSitesSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/sharepoint'
|
||||
import {
|
||||
slackChannelsSelectorContract,
|
||||
slackUserSelectorContract,
|
||||
slackUsersSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/slack'
|
||||
import { trelloBoardsSelectorContract } from '@/lib/api/contracts/selectors/trello'
|
||||
import {
|
||||
wealthboxItemContract,
|
||||
wealthboxItemsSelectorContract,
|
||||
wealthboxOAuthItemContract,
|
||||
wealthboxOAuthItemsContract,
|
||||
} from '@/lib/api/contracts/selectors/wealthbox'
|
||||
import {
|
||||
webflowCollectionsSelectorContract,
|
||||
webflowItemsSelectorContract,
|
||||
webflowSitesSelectorContract,
|
||||
} from '@/lib/api/contracts/selectors/webflow'
|
||||
import { zoomMeetingsSelectorContract } from '@/lib/api/contracts/selectors/zoom'
|
||||
|
||||
export * from '@/lib/api/contracts/selectors/airtable'
|
||||
export * from '@/lib/api/contracts/selectors/asana'
|
||||
export * from '@/lib/api/contracts/selectors/attio'
|
||||
export * from '@/lib/api/contracts/selectors/bigquery'
|
||||
export * from '@/lib/api/contracts/selectors/calcom'
|
||||
export * from '@/lib/api/contracts/selectors/cloudwatch'
|
||||
export * from '@/lib/api/contracts/selectors/confluence'
|
||||
export * from '@/lib/api/contracts/selectors/google'
|
||||
export * from '@/lib/api/contracts/selectors/hubspot'
|
||||
export * from '@/lib/api/contracts/selectors/jira'
|
||||
export * from '@/lib/api/contracts/selectors/jsm'
|
||||
export * from '@/lib/api/contracts/selectors/knowledge'
|
||||
export * from '@/lib/api/contracts/selectors/linear'
|
||||
export * from '@/lib/api/contracts/selectors/microsoft'
|
||||
export * from '@/lib/api/contracts/selectors/monday'
|
||||
export * from '@/lib/api/contracts/selectors/notion'
|
||||
export * from '@/lib/api/contracts/selectors/oauth'
|
||||
export * from '@/lib/api/contracts/selectors/pipedrive'
|
||||
export * from '@/lib/api/contracts/selectors/sharepoint'
|
||||
export * from '@/lib/api/contracts/selectors/slack'
|
||||
export * from '@/lib/api/contracts/selectors/trello'
|
||||
export * from '@/lib/api/contracts/selectors/wealthbox'
|
||||
export * from '@/lib/api/contracts/selectors/webflow'
|
||||
export * from '@/lib/api/contracts/selectors/zoom'
|
||||
|
||||
export const selectorContractsByPath = {
|
||||
'/api/tools/airtable/bases': airtableBasesSelectorContract,
|
||||
'/api/tools/airtable/tables': airtableTablesSelectorContract,
|
||||
'/api/tools/asana/workspaces': asanaWorkspacesSelectorContract,
|
||||
'/api/tools/attio/objects': attioObjectsSelectorContract,
|
||||
'/api/tools/attio/lists': attioListsSelectorContract,
|
||||
'/api/tools/google_bigquery/datasets': bigQueryDatasetsSelectorContract,
|
||||
'/api/tools/google_bigquery/tables': bigQueryTablesSelectorContract,
|
||||
'/api/tools/calcom/event-types': calcomEventTypesSelectorContract,
|
||||
'/api/tools/calcom/schedules': calcomSchedulesSelectorContract,
|
||||
'/api/tools/confluence/selector-spaces': confluenceSpacesSelectorContract,
|
||||
'/api/tools/jsm/selector-servicedesks': jsmServiceDesksSelectorContract,
|
||||
'/api/tools/jsm/selector-requesttypes': jsmRequestTypesSelectorContract,
|
||||
'/api/tools/google_tasks/task-lists': googleTasksTaskListsSelectorContract,
|
||||
'/api/tools/microsoft_planner/plans': microsoftPlannerPlansSelectorContract,
|
||||
'/api/tools/microsoft_planner/tasks': microsoftPlannerTasksSelectorContract,
|
||||
'/api/tools/notion/databases': notionDatabasesSelectorContract,
|
||||
'/api/tools/notion/pages': notionPagesSelectorContract,
|
||||
'/api/tools/pipedrive/pipelines': pipedrivePipelinesSelectorContract,
|
||||
'/api/tools/sharepoint/lists': sharepointListsSelectorContract,
|
||||
'/api/tools/sharepoint/site': sharepointSiteSelectorContract,
|
||||
'/api/tools/sharepoint/sites': sharepointSitesSelectorContract,
|
||||
'/api/tools/trello/boards': trelloBoardsSelectorContract,
|
||||
'/api/tools/zoom/meetings': zoomMeetingsSelectorContract,
|
||||
'/api/tools/slack/channels': slackChannelsSelectorContract,
|
||||
'/api/tools/slack/users': slackUsersSelectorContract,
|
||||
'/api/tools/slack/users:detail': slackUserSelectorContract,
|
||||
'/api/tools/gmail/labels': gmailLabelsSelectorContract,
|
||||
'/api/tools/gmail/label': gmailLabelSelectorContract,
|
||||
'/api/tools/hubspot/properties': hubspotPropertiesSelectorContract,
|
||||
'/api/tools/hubspot/lists': hubspotListsSelectorContract,
|
||||
'/api/tools/hubspot/pipelines': hubspotPipelinesSelectorContract,
|
||||
'/api/tools/hubspot/owners': hubspotOwnersSelectorContract,
|
||||
'/api/tools/outlook/folders': outlookFoldersSelectorContract,
|
||||
'/api/tools/google_calendar/calendars': googleCalendarSelectorContract,
|
||||
'/api/tools/microsoft-teams/teams': microsoftTeamsSelectorContract,
|
||||
'/api/tools/microsoft-teams/chats': microsoftChatsSelectorContract,
|
||||
'/api/tools/microsoft-teams/channels': microsoftChannelsSelectorContract,
|
||||
'/api/tools/wealthbox/items': wealthboxItemsSelectorContract,
|
||||
'/api/tools/wealthbox/item': wealthboxItemContract,
|
||||
'/api/auth/oauth/wealthbox/items': wealthboxOAuthItemsContract,
|
||||
'/api/auth/oauth/wealthbox/item': wealthboxOAuthItemContract,
|
||||
'/api/tools/jira/projects': jiraProjectsSelectorContract,
|
||||
'/api/tools/jira/projects:POST': jiraProjectSelectorContract,
|
||||
'/api/tools/jira/issues': jiraIssuesSelectorContract,
|
||||
'/api/tools/jira/issues:POST': jiraIssueSelectorContract,
|
||||
'/api/tools/monday/boards': mondayBoardsSelectorContract,
|
||||
'/api/tools/monday/groups': mondayGroupsSelectorContract,
|
||||
'/api/tools/linear/teams': linearTeamsSelectorContract,
|
||||
'/api/tools/linear/projects': linearProjectsSelectorContract,
|
||||
'/api/tools/confluence/pages': confluencePagesSelectorContract,
|
||||
'/api/tools/confluence/page': confluencePageSelectorContract,
|
||||
'/api/tools/onedrive/files': onedriveFilesSelectorContract,
|
||||
'/api/tools/onedrive/folder': onedriveFolderSelectorContract,
|
||||
'/api/tools/onedrive/folders': onedriveFoldersSelectorContract,
|
||||
'/api/tools/drive/files': googleDriveFilesSelectorContract,
|
||||
'/api/tools/drive/file': googleDriveFileSelectorContract,
|
||||
'/api/tools/google_sheets/sheets': googleSheetsSelectorContract,
|
||||
'/api/tools/microsoft_excel/sheets': microsoftExcelSheetsSelectorContract,
|
||||
'/api/tools/microsoft_excel/drives': microsoftExcelDrivesSelectorContract,
|
||||
'/api/tools/microsoft_excel/drives:detail': microsoftExcelDriveSelectorContract,
|
||||
'/api/auth/oauth/microsoft/file': microsoftFileSelectorContract,
|
||||
'/api/auth/oauth/microsoft/files': microsoftFilesSelectorContract,
|
||||
'/api/tools/webflow/sites': webflowSitesSelectorContract,
|
||||
'/api/tools/webflow/collections': webflowCollectionsSelectorContract,
|
||||
'/api/tools/webflow/items': webflowItemsSelectorContract,
|
||||
'/api/tools/cloudwatch/describe-log-groups': cloudwatchLogGroupsSelectorContract,
|
||||
'/api/tools/cloudwatch/describe-log-streams': cloudwatchLogStreamsSelectorContract,
|
||||
} as const
|
||||
|
||||
export type SelectorContractPath = keyof typeof selectorContractsByPath
|
||||
@@ -0,0 +1,258 @@
|
||||
import { z } from 'zod'
|
||||
import { idNameSchema, optionalString } from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractBody, ContractJsonResponse, ContractQuery } from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
|
||||
|
||||
const jiraIssueSectionSchema = z
|
||||
.object({
|
||||
issues: z.array(
|
||||
z
|
||||
.object({
|
||||
id: z.string().optional(),
|
||||
key: z.string().optional(),
|
||||
summary: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const jiraProjectsQuerySchema = z.object({
|
||||
domain: z.string().trim().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: optionalString,
|
||||
query: optionalString,
|
||||
})
|
||||
|
||||
export const jiraProjectBodySchema = z.object({
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: optionalString,
|
||||
projectId: z.string().min(1, 'Project ID is required'),
|
||||
})
|
||||
|
||||
/**
|
||||
* GET `/api/tools/jira/issues` query.
|
||||
*/
|
||||
export const jiraIssuesQuerySchema = z.object({
|
||||
domain: z.string().trim().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: optionalString,
|
||||
projectId: optionalString,
|
||||
manualProjectId: optionalString,
|
||||
query: optionalString,
|
||||
all: z
|
||||
.preprocess(
|
||||
(value) => (typeof value === 'string' ? value.toLowerCase() === 'true' : value),
|
||||
z.boolean()
|
||||
)
|
||||
.default(false),
|
||||
limit: z
|
||||
.preprocess((value) => {
|
||||
const parsed = typeof value === 'string' ? Number.parseInt(value, 10) : value
|
||||
return typeof parsed === 'number' && Number.isFinite(parsed) && parsed > 0 ? parsed : 0
|
||||
}, z.number())
|
||||
.default(0),
|
||||
})
|
||||
|
||||
export const jiraIssuesBodySchema = z.object({
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: optionalString,
|
||||
issueKeys: z.array(z.string().min(1)).default([]),
|
||||
})
|
||||
|
||||
export const jiraParentReferenceSchema = z.union([
|
||||
z.string().min(1),
|
||||
z.object({ key: z.string().min(1) }).passthrough(),
|
||||
z.object({ id: z.string().min(1) }).passthrough(),
|
||||
])
|
||||
export type JiraParentReference = z.input<typeof jiraParentReferenceSchema>
|
||||
|
||||
export const jiraWriteBodySchema = z.object({
|
||||
domain: z.string({ error: 'Domain is required' }).min(1, 'Domain is required'),
|
||||
accessToken: z.string({ error: 'Access token is required' }).min(1, 'Access token is required'),
|
||||
projectId: z.string({ error: 'Project ID is required' }).min(1, 'Project ID is required'),
|
||||
summary: z.string({ error: 'Summary is required' }).min(1, 'Summary is required'),
|
||||
description: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
|
||||
priority: z.string().optional(),
|
||||
assignee: z.string().optional(),
|
||||
cloudId: z.string().optional(),
|
||||
issueType: z.string().optional(),
|
||||
parent: jiraParentReferenceSchema.optional(),
|
||||
labels: z.array(z.string()).optional(),
|
||||
duedate: z.string().optional(),
|
||||
reporter: z.string().optional(),
|
||||
environment: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
|
||||
customFieldId: z.string().optional(),
|
||||
customFieldValue: z.string().optional(),
|
||||
components: z.array(z.string()).optional(),
|
||||
fixVersions: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export const jiraUpdateBodySchema = z.object({
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
issueKey: z.string().min(1, 'Issue key is required'),
|
||||
summary: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
description: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
|
||||
priority: z.string().optional(),
|
||||
assignee: z.string().optional(),
|
||||
labels: z.array(z.string()).optional(),
|
||||
components: z.array(z.string()).optional(),
|
||||
duedate: z.string().optional(),
|
||||
fixVersions: z.array(z.string()).optional(),
|
||||
environment: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
|
||||
customFieldId: z.string().optional(),
|
||||
customFieldValue: z.string().optional(),
|
||||
notifyUsers: z.boolean().optional(),
|
||||
cloudId: z.string().optional(),
|
||||
})
|
||||
|
||||
export const jiraAddAttachmentBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
issueKey: z.string().min(1, 'Issue key is required'),
|
||||
files: RawFileInputArraySchema,
|
||||
cloudId: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const jiraProjectsSelectorContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/tools/jira/projects',
|
||||
query: jiraProjectsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z
|
||||
.object({ projects: z.array(idNameSchema), cloudId: z.string().optional() })
|
||||
.passthrough(),
|
||||
},
|
||||
})
|
||||
|
||||
export const jiraProjectSelectorContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/jira/projects',
|
||||
body: jiraProjectBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z
|
||||
.object({ project: idNameSchema.optional(), cloudId: z.string().optional() })
|
||||
.passthrough(),
|
||||
},
|
||||
})
|
||||
|
||||
export const jiraIssuesSelectorContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/tools/jira/issues',
|
||||
query: jiraIssuesQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z
|
||||
.object({
|
||||
sections: z.array(jiraIssueSectionSchema).optional(),
|
||||
cloudId: z.string().optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
},
|
||||
})
|
||||
|
||||
export const jiraIssueSelectorContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/jira/issues',
|
||||
body: jiraIssuesBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z
|
||||
.object({ issues: z.array(idNameSchema).optional(), cloudId: z.string().optional() })
|
||||
.passthrough(),
|
||||
},
|
||||
})
|
||||
|
||||
const jiraWriteResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
output: z.object({
|
||||
ts: z.string(),
|
||||
id: z.string(),
|
||||
issueKey: z.string(),
|
||||
self: z.string(),
|
||||
summary: z.string(),
|
||||
success: z.literal(true),
|
||||
url: z.string(),
|
||||
assigneeId: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
|
||||
const jiraUpdateResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
output: z.object({
|
||||
ts: z.string(),
|
||||
issueKey: z.string(),
|
||||
summary: z.string(),
|
||||
success: z.literal(true),
|
||||
}),
|
||||
})
|
||||
|
||||
const jiraAttachmentSchema = z.object({
|
||||
id: z.string(),
|
||||
filename: z.string(),
|
||||
mimeType: z.string(),
|
||||
size: z.number(),
|
||||
content: z.string(),
|
||||
})
|
||||
|
||||
const jiraAddAttachmentUserFileSchema = z
|
||||
.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string(),
|
||||
url: z.string().optional(),
|
||||
size: z.number(),
|
||||
type: z.string().optional(),
|
||||
key: z.string(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const jiraAddAttachmentResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
output: z.object({
|
||||
ts: z.string(),
|
||||
issueKey: z.string(),
|
||||
attachments: z.array(jiraAttachmentSchema),
|
||||
attachmentIds: z.array(z.string()),
|
||||
files: z.array(jiraAddAttachmentUserFileSchema),
|
||||
}),
|
||||
})
|
||||
|
||||
export const jiraWriteContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/jira/write',
|
||||
body: jiraWriteBodySchema,
|
||||
response: { mode: 'json', schema: jiraWriteResponseSchema },
|
||||
})
|
||||
|
||||
export const jiraUpdateContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/tools/jira/update',
|
||||
body: jiraUpdateBodySchema,
|
||||
response: { mode: 'json', schema: jiraUpdateResponseSchema },
|
||||
})
|
||||
|
||||
export const jiraAddAttachmentContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/jira/add-attachment',
|
||||
body: jiraAddAttachmentBodySchema,
|
||||
response: { mode: 'json', schema: jiraAddAttachmentResponseSchema },
|
||||
})
|
||||
|
||||
export type JiraProjectsQuery = ContractQuery<typeof jiraProjectsSelectorContract>
|
||||
export type JiraProjectBody = ContractBody<typeof jiraProjectSelectorContract>
|
||||
export type JiraIssuesQuery = ContractQuery<typeof jiraIssuesSelectorContract>
|
||||
export type JiraIssuesBody = ContractBody<typeof jiraIssueSelectorContract>
|
||||
export type JiraWriteBody = ContractBody<typeof jiraWriteContract>
|
||||
export type JiraUpdateBody = ContractBody<typeof jiraUpdateContract>
|
||||
export type JiraAddAttachmentBody = ContractBody<typeof jiraAddAttachmentContract>
|
||||
export type JiraProjectsSelectorResponse = ContractJsonResponse<typeof jiraProjectsSelectorContract>
|
||||
export type JiraProjectSelectorResponse = ContractJsonResponse<typeof jiraProjectSelectorContract>
|
||||
export type JiraIssuesSelectorResponse = ContractJsonResponse<typeof jiraIssuesSelectorContract>
|
||||
export type JiraIssueSelectorResponse = ContractJsonResponse<typeof jiraIssueSelectorContract>
|
||||
@@ -0,0 +1,462 @@
|
||||
import { isRecordLike } from '@sim/utils/object'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialWorkflowDomainBodySchema,
|
||||
definePostSelector,
|
||||
idNameSchema,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractBody, ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
|
||||
const jsmBaseBodySchema = z.object({
|
||||
domain: z.string({ error: 'Domain is required' }).min(1, 'Domain is required'),
|
||||
accessToken: z.string({ error: 'Access token is required' }).min(1, 'Access token is required'),
|
||||
cloudId: z.string().optional(),
|
||||
})
|
||||
|
||||
const jsmIssueIdOrKeyField = z
|
||||
.string({ error: 'Issue ID or key is required' })
|
||||
.min(1, 'Issue ID or key is required')
|
||||
|
||||
const jsmServiceDeskIdField = z
|
||||
.string({ error: 'Service Desk ID is required' })
|
||||
.min(1, 'Service Desk ID is required')
|
||||
|
||||
const jsmFormIdField = z.string({ error: 'Form ID is required' }).min(1, 'Form ID is required')
|
||||
|
||||
const jsmIdListSchema = z.union([z.string(), z.array(z.string())]).optional()
|
||||
|
||||
export const jsmRequestTypesBodySchema = credentialWorkflowDomainBodySchema.extend({
|
||||
serviceDeskId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const jsmServiceDesksBodySchema = jsmBaseBodySchema.extend({
|
||||
expand: z.string().optional(),
|
||||
start: z.string().optional(),
|
||||
limit: z.string().optional(),
|
||||
})
|
||||
|
||||
export const jsmServiceDeskScopedBodySchema = jsmBaseBodySchema.extend({
|
||||
serviceDeskId: jsmServiceDeskIdField,
|
||||
start: z.string().optional(),
|
||||
limit: z.string().optional(),
|
||||
})
|
||||
|
||||
export const jsmQueuesBodySchema = jsmServiceDeskScopedBodySchema.extend({
|
||||
includeCount: z.string().optional(),
|
||||
})
|
||||
|
||||
export const jsmRequestTypesToolBodySchema = jsmServiceDeskScopedBodySchema.extend({
|
||||
searchQuery: z.string().optional(),
|
||||
groupId: z.string().optional(),
|
||||
expand: z.string().optional(),
|
||||
})
|
||||
|
||||
export const jsmRequestTypeFieldsBodySchema = jsmBaseBodySchema.extend({
|
||||
serviceDeskId: jsmServiceDeskIdField,
|
||||
requestTypeId: z
|
||||
.string({ error: 'Request Type ID is required' })
|
||||
.min(1, 'Request Type ID is required'),
|
||||
})
|
||||
|
||||
export const jsmRequestsBodySchema = jsmBaseBodySchema.extend({
|
||||
serviceDeskId: z.string().optional(),
|
||||
requestOwnership: z.string().optional(),
|
||||
requestStatus: z.string().optional(),
|
||||
requestTypeId: z.string().optional(),
|
||||
searchTerm: z.string().optional(),
|
||||
expand: z.string().optional(),
|
||||
start: z.string().optional(),
|
||||
limit: z.string().optional(),
|
||||
})
|
||||
|
||||
export const jsmRequestBodySchema = jsmBaseBodySchema.extend({
|
||||
issueIdOrKey: z.string().optional(),
|
||||
serviceDeskId: z.string().optional(),
|
||||
requestTypeId: z.string().optional(),
|
||||
summary: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
raiseOnBehalfOf: z.string().optional(),
|
||||
requestFieldValues: z.record(z.string(), z.unknown()).optional(),
|
||||
formAnswers: z.record(z.string(), z.unknown()).optional(),
|
||||
requestParticipants: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
channel: z.string().optional(),
|
||||
expand: z.string().optional(),
|
||||
})
|
||||
|
||||
export const jsmCommentBodySchema = jsmBaseBodySchema.extend({
|
||||
issueIdOrKey: jsmIssueIdOrKeyField,
|
||||
body: z.string({ error: 'Comment body is required' }).min(1, 'Comment body is required'),
|
||||
isPublic: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const jsmCommentsBodySchema = jsmBaseBodySchema.extend({
|
||||
issueIdOrKey: jsmIssueIdOrKeyField,
|
||||
isPublic: z.boolean().optional(),
|
||||
internal: z.boolean().optional(),
|
||||
expand: z.string().optional(),
|
||||
start: z.string().optional(),
|
||||
limit: z.string().optional(),
|
||||
})
|
||||
|
||||
export const jsmTransitionBodySchema = jsmBaseBodySchema.extend({
|
||||
issueIdOrKey: jsmIssueIdOrKeyField,
|
||||
transitionId: z
|
||||
.string({ error: 'Transition ID is required' })
|
||||
.min(1, 'Transition ID is required'),
|
||||
comment: z.string().optional(),
|
||||
})
|
||||
|
||||
export const jsmIssuePaginationBodySchema = jsmBaseBodySchema.extend({
|
||||
issueIdOrKey: jsmIssueIdOrKeyField,
|
||||
start: z.string().optional(),
|
||||
limit: z.string().optional(),
|
||||
})
|
||||
|
||||
export const jsmApprovalsBodySchema = jsmBaseBodySchema.extend({
|
||||
action: z.string({ error: 'Action is required' }).min(1, 'Action is required'),
|
||||
issueIdOrKey: jsmIssueIdOrKeyField,
|
||||
approvalId: z.string().optional(),
|
||||
decision: z.string().optional(),
|
||||
start: z.string().optional(),
|
||||
limit: z.string().optional(),
|
||||
})
|
||||
|
||||
export const jsmParticipantsBodySchema = jsmBaseBodySchema.extend({
|
||||
action: z.string({ error: 'Action is required' }).min(1, 'Action is required'),
|
||||
issueIdOrKey: jsmIssueIdOrKeyField,
|
||||
accountIds: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
start: z.string().optional(),
|
||||
limit: z.string().optional(),
|
||||
})
|
||||
|
||||
export const jsmCustomersBodySchema = jsmBaseBodySchema.extend({
|
||||
serviceDeskId: jsmServiceDeskIdField,
|
||||
query: z.string().optional(),
|
||||
start: z.string().optional(),
|
||||
limit: z.string().optional(),
|
||||
accountIds: jsmIdListSchema,
|
||||
emails: jsmIdListSchema,
|
||||
})
|
||||
|
||||
export const jsmOrganizationBodySchema = jsmBaseBodySchema.extend({
|
||||
action: z.string({ error: 'Action is required' }).min(1, 'Action is required'),
|
||||
name: z.string().optional(),
|
||||
serviceDeskId: z.string().optional(),
|
||||
organizationId: z.string().optional(),
|
||||
})
|
||||
|
||||
export const jsmIssueFormsBodySchema = jsmBaseBodySchema.extend({
|
||||
issueIdOrKey: jsmIssueIdOrKeyField,
|
||||
})
|
||||
|
||||
export const jsmIssueFormBodySchema = jsmIssueFormsBodySchema.extend({
|
||||
formId: jsmFormIdField,
|
||||
})
|
||||
|
||||
export const jsmAttachFormBodySchema = jsmIssueFormsBodySchema.extend({
|
||||
formTemplateId: z
|
||||
.string({ error: 'Form template ID is required' })
|
||||
.min(1, 'Form template ID is required'),
|
||||
})
|
||||
|
||||
export const jsmSaveFormAnswersBodySchema = jsmIssueFormBodySchema.extend({
|
||||
answers: z.custom<Record<string, unknown>>(isRecordLike, {
|
||||
message: 'Answers object is required',
|
||||
}),
|
||||
})
|
||||
|
||||
export const jsmProjectFormTemplatesBodySchema = jsmBaseBodySchema.extend({
|
||||
projectIdOrKey: z
|
||||
.string({ error: 'Project ID or key is required' })
|
||||
.min(1, 'Project ID or key is required'),
|
||||
})
|
||||
|
||||
export const jsmProjectFormStructureBodySchema = jsmProjectFormTemplatesBodySchema.extend({
|
||||
formId: jsmFormIdField,
|
||||
})
|
||||
|
||||
export const jsmCopyFormsBodySchema = jsmBaseBodySchema.extend({
|
||||
sourceIssueIdOrKey: z
|
||||
.string({ error: 'Source issue ID or key is required' })
|
||||
.min(1, 'Source issue ID or key is required'),
|
||||
targetIssueIdOrKey: z
|
||||
.string({ error: 'Target issue ID or key is required' })
|
||||
.min(1, 'Target issue ID or key is required'),
|
||||
formIds: z.array(z.string(), { error: 'formIds must be an array of form UUIDs' }).optional(),
|
||||
})
|
||||
|
||||
const jsmAssetsBaseBodySchema = jsmBaseBodySchema.extend({
|
||||
workspaceId: z.string().optional(),
|
||||
})
|
||||
|
||||
const jsmAssetsPaginationField = z.union([z.string(), z.number()]).optional()
|
||||
|
||||
export const jsmListObjectSchemasBodySchema = jsmAssetsBaseBodySchema.extend({
|
||||
startAt: jsmAssetsPaginationField,
|
||||
maxResults: jsmAssetsPaginationField,
|
||||
includeCounts: z.union([z.string(), z.boolean()]).optional(),
|
||||
})
|
||||
|
||||
export const jsmObjectSchemaBodySchema = jsmAssetsBaseBodySchema.extend({
|
||||
schemaId: z.string({ error: 'Schema ID is required' }).min(1, 'Schema ID is required'),
|
||||
})
|
||||
|
||||
export const jsmObjectTypesBodySchema = jsmAssetsBaseBodySchema.extend({
|
||||
schemaId: z.string({ error: 'Schema ID is required' }).min(1, 'Schema ID is required'),
|
||||
excludeAbstract: z.union([z.string(), z.boolean()]).optional(),
|
||||
})
|
||||
|
||||
export const jsmObjectTypeAttributesBodySchema = jsmAssetsBaseBodySchema.extend({
|
||||
objectTypeId: z
|
||||
.string({ error: 'Object type ID is required' })
|
||||
.min(1, 'Object type ID is required'),
|
||||
onlyValueEditable: z.union([z.string(), z.boolean()]).optional(),
|
||||
query: z.string().optional(),
|
||||
})
|
||||
|
||||
export const jsmSearchObjectsAqlBodySchema = jsmAssetsBaseBodySchema.extend({
|
||||
qlQuery: z.string({ error: 'AQL query is required' }).min(1, 'AQL query is required'),
|
||||
page: jsmAssetsPaginationField,
|
||||
resultsPerPage: jsmAssetsPaginationField,
|
||||
includeAttributes: z.union([z.string(), z.boolean()]).optional(),
|
||||
objectTypeId: z.string().optional(),
|
||||
objectSchemaId: z.string().optional(),
|
||||
})
|
||||
|
||||
export const jsmGetObjectBodySchema = jsmAssetsBaseBodySchema.extend({
|
||||
objectId: z.string({ error: 'Object ID is required' }).min(1, 'Object ID is required'),
|
||||
})
|
||||
|
||||
const jsmAssetAttributeInputSchema = z.object({
|
||||
objectTypeAttributeId: z
|
||||
.string({ error: 'objectTypeAttributeId is required' })
|
||||
.min(1, 'objectTypeAttributeId is required'),
|
||||
objectAttributeValues: z
|
||||
.array(z.object({ value: z.unknown() }), {
|
||||
error: 'objectAttributeValues must be an array of { value } entries',
|
||||
})
|
||||
.min(1, 'Each attribute needs at least one value'),
|
||||
})
|
||||
|
||||
export const jsmCreateObjectBodySchema = jsmAssetsBaseBodySchema.extend({
|
||||
objectTypeId: z
|
||||
.string({ error: 'Object type ID is required' })
|
||||
.min(1, 'Object type ID is required'),
|
||||
attributes: z
|
||||
.array(jsmAssetAttributeInputSchema, { error: 'attributes is required' })
|
||||
.min(1, 'At least one attribute is required'),
|
||||
})
|
||||
|
||||
export const jsmUpdateObjectBodySchema = jsmAssetsBaseBodySchema.extend({
|
||||
objectId: z.string({ error: 'Object ID is required' }).min(1, 'Object ID is required'),
|
||||
objectTypeId: z.string().optional(),
|
||||
attributes: z
|
||||
.array(jsmAssetAttributeInputSchema, { error: 'attributes is required' })
|
||||
.min(1, 'At least one attribute is required'),
|
||||
})
|
||||
|
||||
export const jsmDeleteObjectBodySchema = jsmAssetsBaseBodySchema.extend({
|
||||
objectId: z.string({ error: 'Object ID is required' }).min(1, 'Object ID is required'),
|
||||
})
|
||||
|
||||
export const defineJsmToolContract = <TBody extends z.ZodType>(path: string, body: TBody) =>
|
||||
definePostSelector(path, body, z.unknown())
|
||||
|
||||
export const jsmServiceDesksSelectorContract = definePostSelector(
|
||||
'/api/tools/jsm/selector-servicedesks',
|
||||
credentialWorkflowDomainBodySchema,
|
||||
z.object({ serviceDesks: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
export const jsmRequestTypesSelectorContract = definePostSelector(
|
||||
'/api/tools/jsm/selector-requesttypes',
|
||||
jsmRequestTypesBodySchema,
|
||||
z.object({ requestTypes: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
export const jsmServiceDesksContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/servicedesks',
|
||||
jsmServiceDesksBodySchema
|
||||
)
|
||||
export const jsmQueuesContract = defineJsmToolContract('/api/tools/jsm/queues', jsmQueuesBodySchema)
|
||||
export const jsmRequestTypesContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/requesttypes',
|
||||
jsmRequestTypesToolBodySchema
|
||||
)
|
||||
export const jsmRequestTypeFieldsContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/requesttypefields',
|
||||
jsmRequestTypeFieldsBodySchema
|
||||
)
|
||||
export const jsmRequestsContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/requests',
|
||||
jsmRequestsBodySchema
|
||||
)
|
||||
export const jsmRequestContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/request',
|
||||
jsmRequestBodySchema
|
||||
)
|
||||
export const jsmCommentContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/comment',
|
||||
jsmCommentBodySchema
|
||||
)
|
||||
export const jsmCommentsContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/comments',
|
||||
jsmCommentsBodySchema
|
||||
)
|
||||
export const jsmTransitionContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/transition',
|
||||
jsmTransitionBodySchema
|
||||
)
|
||||
export const jsmSlaContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/sla',
|
||||
jsmIssuePaginationBodySchema
|
||||
)
|
||||
export const jsmTransitionsContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/transitions',
|
||||
jsmIssuePaginationBodySchema
|
||||
)
|
||||
export const jsmApprovalsContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/approvals',
|
||||
jsmApprovalsBodySchema
|
||||
)
|
||||
export const jsmParticipantsContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/participants',
|
||||
jsmParticipantsBodySchema
|
||||
)
|
||||
export const jsmCustomersContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/customers',
|
||||
jsmCustomersBodySchema
|
||||
)
|
||||
export const jsmOrganizationsContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/organizations',
|
||||
jsmServiceDeskScopedBodySchema
|
||||
)
|
||||
export const jsmOrganizationContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/organization',
|
||||
jsmOrganizationBodySchema
|
||||
)
|
||||
export const jsmIssueFormsContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/forms/issue',
|
||||
jsmIssueFormsBodySchema
|
||||
)
|
||||
export const jsmAttachFormContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/forms/attach',
|
||||
jsmAttachFormBodySchema
|
||||
)
|
||||
export const jsmGetFormContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/forms/get',
|
||||
jsmIssueFormBodySchema
|
||||
)
|
||||
export const jsmSubmitFormContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/forms/submit',
|
||||
jsmIssueFormBodySchema
|
||||
)
|
||||
export const jsmDeleteFormContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/forms/delete',
|
||||
jsmIssueFormBodySchema
|
||||
)
|
||||
export const jsmExternaliseFormContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/forms/externalise',
|
||||
jsmIssueFormBodySchema
|
||||
)
|
||||
export const jsmInternaliseFormContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/forms/internalise',
|
||||
jsmIssueFormBodySchema
|
||||
)
|
||||
export const jsmReopenFormContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/forms/reopen',
|
||||
jsmIssueFormBodySchema
|
||||
)
|
||||
export const jsmSaveFormAnswersContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/forms/save',
|
||||
jsmSaveFormAnswersBodySchema
|
||||
)
|
||||
export const jsmFormAnswersContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/forms/answers',
|
||||
jsmIssueFormBodySchema
|
||||
)
|
||||
export const jsmProjectFormTemplatesContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/forms/templates',
|
||||
jsmProjectFormTemplatesBodySchema
|
||||
)
|
||||
export const jsmProjectFormStructureContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/forms/structure',
|
||||
jsmProjectFormStructureBodySchema
|
||||
)
|
||||
export const jsmCopyFormsContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/forms/copy',
|
||||
jsmCopyFormsBodySchema
|
||||
)
|
||||
|
||||
export const jsmListObjectSchemasContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/assets/schemas',
|
||||
jsmListObjectSchemasBodySchema
|
||||
)
|
||||
export const jsmGetObjectSchemaContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/assets/schema',
|
||||
jsmObjectSchemaBodySchema
|
||||
)
|
||||
export const jsmListObjectTypesContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/assets/object-types',
|
||||
jsmObjectTypesBodySchema
|
||||
)
|
||||
export const jsmObjectTypeAttributesContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/assets/attributes',
|
||||
jsmObjectTypeAttributesBodySchema
|
||||
)
|
||||
export const jsmSearchObjectsAqlContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/assets/search',
|
||||
jsmSearchObjectsAqlBodySchema
|
||||
)
|
||||
export const jsmGetObjectContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/assets/object/get',
|
||||
jsmGetObjectBodySchema
|
||||
)
|
||||
export const jsmCreateObjectContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/assets/object/create',
|
||||
jsmCreateObjectBodySchema
|
||||
)
|
||||
export const jsmUpdateObjectContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/assets/object/update',
|
||||
jsmUpdateObjectBodySchema
|
||||
)
|
||||
export const jsmDeleteObjectContract = defineJsmToolContract(
|
||||
'/api/tools/jsm/assets/object/delete',
|
||||
jsmDeleteObjectBodySchema
|
||||
)
|
||||
|
||||
export type JsmServiceDesksBody = ContractBody<typeof jsmServiceDesksContract>
|
||||
export type JsmQueuesBody = ContractBody<typeof jsmQueuesContract>
|
||||
export type JsmRequestTypesBody = ContractBody<typeof jsmRequestTypesContract>
|
||||
export type JsmRequestTypeFieldsBody = ContractBody<typeof jsmRequestTypeFieldsContract>
|
||||
export type JsmRequestsBody = ContractBody<typeof jsmRequestsContract>
|
||||
export type JsmRequestBody = ContractBody<typeof jsmRequestContract>
|
||||
export type JsmCommentBody = ContractBody<typeof jsmCommentContract>
|
||||
export type JsmCommentsBody = ContractBody<typeof jsmCommentsContract>
|
||||
export type JsmTransitionBody = ContractBody<typeof jsmTransitionContract>
|
||||
export type JsmSlaBody = ContractBody<typeof jsmSlaContract>
|
||||
export type JsmTransitionsBody = ContractBody<typeof jsmTransitionsContract>
|
||||
export type JsmApprovalsBody = ContractBody<typeof jsmApprovalsContract>
|
||||
export type JsmParticipantsBody = ContractBody<typeof jsmParticipantsContract>
|
||||
export type JsmCustomersBody = ContractBody<typeof jsmCustomersContract>
|
||||
export type JsmOrganizationsBody = ContractBody<typeof jsmOrganizationsContract>
|
||||
export type JsmOrganizationBody = ContractBody<typeof jsmOrganizationContract>
|
||||
export type JsmIssueFormsBody = ContractBody<typeof jsmIssueFormsContract>
|
||||
export type JsmAttachFormBody = ContractBody<typeof jsmAttachFormContract>
|
||||
export type JsmGetFormBody = ContractBody<typeof jsmGetFormContract>
|
||||
export type JsmSubmitFormBody = ContractBody<typeof jsmSubmitFormContract>
|
||||
export type JsmDeleteFormBody = ContractBody<typeof jsmDeleteFormContract>
|
||||
export type JsmExternaliseFormBody = ContractBody<typeof jsmExternaliseFormContract>
|
||||
export type JsmInternaliseFormBody = ContractBody<typeof jsmInternaliseFormContract>
|
||||
export type JsmReopenFormBody = ContractBody<typeof jsmReopenFormContract>
|
||||
export type JsmSaveFormAnswersBody = ContractBody<typeof jsmSaveFormAnswersContract>
|
||||
export type JsmFormAnswersBody = ContractBody<typeof jsmFormAnswersContract>
|
||||
export type JsmProjectFormTemplatesBody = ContractBody<typeof jsmProjectFormTemplatesContract>
|
||||
export type JsmProjectFormStructureBody = ContractBody<typeof jsmProjectFormStructureContract>
|
||||
export type JsmCopyFormsBody = ContractBody<typeof jsmCopyFormsContract>
|
||||
export type JsmServiceDesksSelectorResponse = ContractJsonResponse<
|
||||
typeof jsmServiceDesksSelectorContract
|
||||
>
|
||||
export type JsmRequestTypesSelectorResponse = ContractJsonResponse<
|
||||
typeof jsmRequestTypesSelectorContract
|
||||
>
|
||||
@@ -0,0 +1,69 @@
|
||||
import { z } from 'zod'
|
||||
import { optionalString } from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const knowledgeDocumentsParamsSchema = z.object({ id: z.string().min(1) })
|
||||
|
||||
const knowledgeDocumentParamsSchema = knowledgeDocumentsParamsSchema.extend({
|
||||
documentId: z.string().min(1),
|
||||
})
|
||||
|
||||
const knowledgeDocumentsQuerySchema = z.object({
|
||||
limit: z.coerce.number().int().min(1).max(100).optional(),
|
||||
offset: z.coerce.number().int().min(0).optional(),
|
||||
search: optionalString,
|
||||
})
|
||||
|
||||
const knowledgeDocumentQuerySchema = z.object({
|
||||
includeDisabled: optionalString,
|
||||
})
|
||||
|
||||
const knowledgeDocumentSchema = z.object({ id: z.string(), filename: z.string() }).passthrough()
|
||||
|
||||
export const listKnowledgeSelectorDocumentsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/knowledge/[id]/documents',
|
||||
params: knowledgeDocumentsParamsSchema,
|
||||
query: knowledgeDocumentsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
data: z
|
||||
.object({
|
||||
documents: z.array(knowledgeDocumentSchema),
|
||||
pagination: z
|
||||
.object({
|
||||
total: z.number(),
|
||||
limit: z.number(),
|
||||
offset: z.number(),
|
||||
hasMore: z.boolean(),
|
||||
})
|
||||
.passthrough(),
|
||||
})
|
||||
.passthrough(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const getKnowledgeSelectorDocumentContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/knowledge/[id]/documents/[documentId]',
|
||||
params: knowledgeDocumentParamsSchema,
|
||||
query: knowledgeDocumentQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
data: knowledgeDocumentSchema,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export type ListKnowledgeSelectorDocumentsResponse = ContractJsonResponse<
|
||||
typeof listKnowledgeSelectorDocumentsContract
|
||||
>
|
||||
export type GetKnowledgeSelectorDocumentResponse = ContractJsonResponse<
|
||||
typeof getKnowledgeSelectorDocumentContract
|
||||
>
|
||||
@@ -0,0 +1,28 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialWorkflowBodySchema,
|
||||
definePostSelector,
|
||||
idNameSchema,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
|
||||
export const linearProjectsBodySchema = credentialWorkflowBodySchema.extend({
|
||||
teamId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const linearTeamsSelectorContract = definePostSelector(
|
||||
'/api/tools/linear/teams',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ teams: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
export const linearProjectsSelectorContract = definePostSelector(
|
||||
'/api/tools/linear/projects',
|
||||
linearProjectsBodySchema,
|
||||
z.object({ projects: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
export type LinearTeamsSelectorResponse = ContractJsonResponse<typeof linearTeamsSelectorContract>
|
||||
export type LinearProjectsSelectorResponse = ContractJsonResponse<
|
||||
typeof linearProjectsSelectorContract
|
||||
>
|
||||
@@ -0,0 +1,238 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialIdQuerySchema,
|
||||
credentialIdQueryWithSearchSchema,
|
||||
credentialWorkflowBodySchema,
|
||||
defineGetSelector,
|
||||
definePostSelector,
|
||||
fileOptionSchema,
|
||||
idDisplayNameSchema,
|
||||
idNameSchema,
|
||||
idTitleSchema,
|
||||
optionalString,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractBody, ContractJsonResponse, ContractQuery } from '@/lib/api/contracts/types'
|
||||
|
||||
export const teamsChannelsBodySchema = credentialWorkflowBodySchema.extend({
|
||||
teamId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const plannerTasksBodySchema = credentialWorkflowBodySchema.extend({
|
||||
planId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const microsoftExcelSheetsQuerySchema = credentialIdQuerySchema.extend({
|
||||
spreadsheetId: z.string().min(1, 'Spreadsheet ID is required'),
|
||||
driveId: optionalString,
|
||||
workflowId: optionalString,
|
||||
})
|
||||
|
||||
/**
|
||||
* Body for `POST /api/tools/microsoft_excel/drives`. The route serves both
|
||||
* list-drives (no `driveId`) and single-drive lookup (`driveId` provided),
|
||||
* dispatching at runtime. The contract permits the optional `driveId` so a
|
||||
* single body schema covers both flows.
|
||||
*/
|
||||
export const microsoftExcelDrivesBodySchema = credentialWorkflowBodySchema.extend({
|
||||
siteId: z.string().min(1, 'Site ID is required'),
|
||||
driveId: optionalString,
|
||||
})
|
||||
|
||||
/**
|
||||
* The `/api/auth/oauth/microsoft/files` route is shared by the
|
||||
* `microsoft.excel` and `microsoft.word` selectors. `fileType` lets the route
|
||||
* search for and filter to the correct Office document type; it is optional and
|
||||
* defaults to `excel` on the server for backward compatibility.
|
||||
*/
|
||||
export const microsoftFilesQuerySchema = credentialIdQuerySchema.extend({
|
||||
query: optionalString,
|
||||
driveId: optionalString,
|
||||
workflowId: optionalString,
|
||||
fileType: z.enum(['excel', 'word']).optional(),
|
||||
})
|
||||
|
||||
export const microsoftFileQuerySchema = credentialIdQuerySchema.extend({
|
||||
fileId: z.string({ error: 'File ID is required' }).min(1, 'File ID is required'),
|
||||
workflowId: optionalString,
|
||||
})
|
||||
|
||||
export const onedriveFolderQuerySchema = z.object({
|
||||
credentialId: z.preprocess(
|
||||
(value) => value ?? '',
|
||||
z.string().min(1, 'Credential ID and File ID are required')
|
||||
),
|
||||
fileId: z.preprocess(
|
||||
(value) => value ?? '',
|
||||
z.string().min(1, 'Credential ID and File ID are required')
|
||||
),
|
||||
})
|
||||
|
||||
export const onedriveFilesQuerySchema = credentialIdQueryWithSearchSchema
|
||||
export const onedriveFoldersQuerySchema = credentialIdQueryWithSearchSchema
|
||||
export const outlookFoldersQuerySchema = credentialIdQuerySchema
|
||||
|
||||
export const outlookFoldersSelectorContract = defineGetSelector(
|
||||
'/api/tools/outlook/folders',
|
||||
outlookFoldersQuerySchema,
|
||||
z.object({ folders: z.array(z.object({ id: z.string(), name: z.string() }).passthrough()) })
|
||||
)
|
||||
|
||||
export const microsoftTeamsSelectorContract = definePostSelector(
|
||||
'/api/tools/microsoft-teams/teams',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ teams: z.array(idDisplayNameSchema) })
|
||||
)
|
||||
|
||||
export const microsoftChatsSelectorContract = definePostSelector(
|
||||
'/api/tools/microsoft-teams/chats',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ chats: z.array(idDisplayNameSchema) })
|
||||
)
|
||||
|
||||
export const microsoftChannelsSelectorContract = definePostSelector(
|
||||
'/api/tools/microsoft-teams/channels',
|
||||
teamsChannelsBodySchema,
|
||||
z.object({ channels: z.array(idDisplayNameSchema) })
|
||||
)
|
||||
|
||||
export const microsoftPlannerPlansSelectorContract = definePostSelector(
|
||||
'/api/tools/microsoft_planner/plans',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ plans: z.array(idTitleSchema) })
|
||||
)
|
||||
|
||||
export const microsoftPlannerTasksSelectorContract = definePostSelector(
|
||||
'/api/tools/microsoft_planner/tasks',
|
||||
plannerTasksBodySchema,
|
||||
z
|
||||
.object({
|
||||
tasks: z.array(idTitleSchema),
|
||||
metadata: z
|
||||
.object({
|
||||
planId: z.string(),
|
||||
planUrl: z.string(),
|
||||
})
|
||||
.passthrough()
|
||||
.optional(),
|
||||
})
|
||||
.passthrough()
|
||||
)
|
||||
|
||||
export const onedriveFilesSelectorContract = defineGetSelector(
|
||||
'/api/tools/onedrive/files',
|
||||
onedriveFilesQuerySchema,
|
||||
z.object({ files: z.array(fileOptionSchema) })
|
||||
)
|
||||
|
||||
export const onedriveFoldersSelectorContract = defineGetSelector(
|
||||
'/api/tools/onedrive/folders',
|
||||
onedriveFoldersQuerySchema,
|
||||
z.object({ files: z.array(fileOptionSchema) })
|
||||
)
|
||||
|
||||
export const onedriveFolderSelectorContract = defineGetSelector(
|
||||
'/api/tools/onedrive/folder',
|
||||
onedriveFolderQuerySchema,
|
||||
z.object({ file: fileOptionSchema.optional() }).passthrough()
|
||||
)
|
||||
|
||||
export const microsoftExcelSheetsSelectorContract = defineGetSelector(
|
||||
'/api/tools/microsoft_excel/sheets',
|
||||
microsoftExcelSheetsQuerySchema,
|
||||
z.object({ sheets: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
export const microsoftExcelDrivesSelectorContract = definePostSelector(
|
||||
'/api/tools/microsoft_excel/drives',
|
||||
microsoftExcelDrivesBodySchema,
|
||||
z.object({ drives: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
/**
|
||||
* Single-drive variant. Same body schema as the list contract; the `driveId`
|
||||
* is what discriminates the response shape at the route layer.
|
||||
*/
|
||||
export const microsoftExcelDriveSelectorContract = definePostSelector(
|
||||
'/api/tools/microsoft_excel/drives',
|
||||
microsoftExcelDrivesBodySchema,
|
||||
z.object({ drive: idNameSchema.optional() })
|
||||
)
|
||||
|
||||
export const microsoftFilesSelectorContract = defineGetSelector(
|
||||
'/api/auth/oauth/microsoft/files',
|
||||
microsoftFilesQuerySchema,
|
||||
z.object({ files: z.array(fileOptionSchema) })
|
||||
)
|
||||
|
||||
export const microsoftFileSelectorContract = defineGetSelector(
|
||||
'/api/auth/oauth/microsoft/file',
|
||||
microsoftFileQuerySchema,
|
||||
z.object({ file: fileOptionSchema.optional() }).passthrough()
|
||||
)
|
||||
|
||||
export type OutlookFoldersSelectorResponse = ContractJsonResponse<
|
||||
typeof outlookFoldersSelectorContract
|
||||
>
|
||||
export type OutlookFoldersSelectorQuery = ContractQuery<typeof outlookFoldersSelectorContract>
|
||||
export type MicrosoftTeamsSelectorResponse = ContractJsonResponse<
|
||||
typeof microsoftTeamsSelectorContract
|
||||
>
|
||||
export type MicrosoftTeamsSelectorBody = ContractBody<typeof microsoftTeamsSelectorContract>
|
||||
export type MicrosoftChatsSelectorResponse = ContractJsonResponse<
|
||||
typeof microsoftChatsSelectorContract
|
||||
>
|
||||
export type MicrosoftChatsSelectorBody = ContractBody<typeof microsoftChatsSelectorContract>
|
||||
export type MicrosoftChannelsSelectorResponse = ContractJsonResponse<
|
||||
typeof microsoftChannelsSelectorContract
|
||||
>
|
||||
export type MicrosoftChannelsSelectorBody = ContractBody<typeof microsoftChannelsSelectorContract>
|
||||
export type MicrosoftPlannerPlansSelectorResponse = ContractJsonResponse<
|
||||
typeof microsoftPlannerPlansSelectorContract
|
||||
>
|
||||
export type MicrosoftPlannerPlansSelectorBody = ContractBody<
|
||||
typeof microsoftPlannerPlansSelectorContract
|
||||
>
|
||||
export type MicrosoftPlannerTasksSelectorResponse = ContractJsonResponse<
|
||||
typeof microsoftPlannerTasksSelectorContract
|
||||
>
|
||||
export type MicrosoftPlannerTasksSelectorBody = ContractBody<
|
||||
typeof microsoftPlannerTasksSelectorContract
|
||||
>
|
||||
export type OnedriveFilesSelectorResponse = ContractJsonResponse<
|
||||
typeof onedriveFilesSelectorContract
|
||||
>
|
||||
export type OnedriveFilesSelectorQuery = ContractQuery<typeof onedriveFilesSelectorContract>
|
||||
export type OnedriveFoldersSelectorResponse = ContractJsonResponse<
|
||||
typeof onedriveFoldersSelectorContract
|
||||
>
|
||||
export type OnedriveFoldersSelectorQuery = ContractQuery<typeof onedriveFoldersSelectorContract>
|
||||
export type OnedriveFolderSelectorResponse = ContractJsonResponse<
|
||||
typeof onedriveFolderSelectorContract
|
||||
>
|
||||
export type OnedriveFolderSelectorQuery = ContractQuery<typeof onedriveFolderSelectorContract>
|
||||
export type MicrosoftExcelSheetsSelectorResponse = ContractJsonResponse<
|
||||
typeof microsoftExcelSheetsSelectorContract
|
||||
>
|
||||
export type MicrosoftExcelSheetsSelectorQuery = ContractQuery<
|
||||
typeof microsoftExcelSheetsSelectorContract
|
||||
>
|
||||
export type MicrosoftExcelDrivesSelectorResponse = ContractJsonResponse<
|
||||
typeof microsoftExcelDrivesSelectorContract
|
||||
>
|
||||
export type MicrosoftExcelDrivesSelectorBody = ContractBody<
|
||||
typeof microsoftExcelDrivesSelectorContract
|
||||
>
|
||||
export type MicrosoftExcelDriveSelectorResponse = ContractJsonResponse<
|
||||
typeof microsoftExcelDriveSelectorContract
|
||||
>
|
||||
export type MicrosoftExcelDriveSelectorBody = ContractBody<
|
||||
typeof microsoftExcelDriveSelectorContract
|
||||
>
|
||||
export type MicrosoftFilesSelectorResponse = ContractJsonResponse<
|
||||
typeof microsoftFilesSelectorContract
|
||||
>
|
||||
export type MicrosoftFilesSelectorQuery = ContractQuery<typeof microsoftFilesSelectorContract>
|
||||
export type MicrosoftFileSelectorResponse = ContractJsonResponse<
|
||||
typeof microsoftFileSelectorContract
|
||||
>
|
||||
export type MicrosoftFileSelectorQuery = ContractQuery<typeof microsoftFileSelectorContract>
|
||||
@@ -0,0 +1,32 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialWorkflowBodySchema,
|
||||
definePostSelector,
|
||||
idNameSchema,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
|
||||
/**
|
||||
* Monday board IDs are numeric in the API (e.g. `123456789`). Clients
|
||||
* sometimes pass them as numbers and sometimes as strings, so we accept
|
||||
* `string | number` and let the route's `validateMondayNumericId` enforce
|
||||
* the actual numeric format.
|
||||
*/
|
||||
export const mondayGroupsBodySchema = credentialWorkflowBodySchema.extend({
|
||||
boardId: z.union([z.string().min(1), z.number()]),
|
||||
})
|
||||
|
||||
export const mondayBoardsSelectorContract = definePostSelector(
|
||||
'/api/tools/monday/boards',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ boards: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
export const mondayGroupsSelectorContract = definePostSelector(
|
||||
'/api/tools/monday/groups',
|
||||
mondayGroupsBodySchema,
|
||||
z.object({ groups: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
export type MondayBoardsSelectorResponse = ContractJsonResponse<typeof mondayBoardsSelectorContract>
|
||||
export type MondayGroupsSelectorResponse = ContractJsonResponse<typeof mondayGroupsSelectorContract>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialWorkflowBodySchema,
|
||||
definePostSelector,
|
||||
idNameSchema,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
|
||||
export const notionDatabasesSelectorContract = definePostSelector(
|
||||
'/api/tools/notion/databases',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ databases: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
export const notionPagesSelectorContract = definePostSelector(
|
||||
'/api/tools/notion/pages',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ pages: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
export type NotionDatabasesSelectorResponse = ContractJsonResponse<
|
||||
typeof notionDatabasesSelectorContract
|
||||
>
|
||||
export type NotionPagesSelectorResponse = ContractJsonResponse<typeof notionPagesSelectorContract>
|
||||
@@ -0,0 +1,22 @@
|
||||
import { z } from 'zod'
|
||||
import { oauthTokenRequestBodySchema } from '@/lib/api/contracts/oauth-connections'
|
||||
import { definePostSelector } from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
|
||||
const oauthTokenResponseSchema = z
|
||||
.object({
|
||||
accessToken: z.string().optional(),
|
||||
idToken: z.string().optional(),
|
||||
instanceUrl: z.string().optional(),
|
||||
cloudId: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const oauthTokenContract = definePostSelector(
|
||||
'/api/auth/oauth/token',
|
||||
oauthTokenRequestBodySchema,
|
||||
oauthTokenResponseSchema
|
||||
)
|
||||
|
||||
export type OauthTokenResponse = ContractJsonResponse<typeof oauthTokenContract>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialWorkflowBodySchema,
|
||||
definePostSelector,
|
||||
idNameSchema,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
|
||||
export const pipedrivePipelinesSelectorContract = definePostSelector(
|
||||
'/api/tools/pipedrive/pipelines',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ pipelines: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
export type PipedrivePipelinesSelectorResponse = ContractJsonResponse<
|
||||
typeof pipedrivePipelinesSelectorContract
|
||||
>
|
||||
@@ -0,0 +1,70 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const optionalString = z.string().optional()
|
||||
|
||||
/**
|
||||
* Accepts `string | null | undefined` on the wire and outputs `string |
|
||||
* undefined` (null collapsed to undefined). Used for fields like `workflowId`
|
||||
* where the wire is permissive but server-side handlers expect an optional
|
||||
* string.
|
||||
*/
|
||||
export const nullableOptionalString = z
|
||||
.string()
|
||||
.nullish()
|
||||
.transform((value) => value ?? undefined)
|
||||
|
||||
export const credentialWorkflowBodySchema = z.object({
|
||||
credential: z.string().min(1),
|
||||
workflowId: nullableOptionalString,
|
||||
})
|
||||
|
||||
export const credentialWorkflowDomainBodySchema = credentialWorkflowBodySchema.extend({
|
||||
domain: z.string().min(1),
|
||||
})
|
||||
|
||||
export const credentialWorkflowImpersonateBodySchema = credentialWorkflowBodySchema.extend({
|
||||
impersonateEmail: optionalString,
|
||||
})
|
||||
|
||||
export const credentialIdQuerySchema = z.object({
|
||||
credentialId: z
|
||||
.string({ error: 'Credential ID is required' })
|
||||
.min(1, 'Credential ID is required'),
|
||||
})
|
||||
|
||||
export const credentialIdQueryWithSearchSchema = credentialIdQuerySchema.extend({
|
||||
query: optionalString,
|
||||
})
|
||||
|
||||
export const idNameSchema = z.object({ id: z.string(), name: z.string() }).passthrough()
|
||||
export const idTitleSchema = z.object({ id: z.string(), title: z.string() }).passthrough()
|
||||
export const idDisplayNameSchema = z
|
||||
.object({ id: z.string(), displayName: z.string() })
|
||||
.passthrough()
|
||||
export const fileOptionSchema = z.object({ id: z.string(), name: z.string() }).passthrough()
|
||||
export const folderOptionSchema = z.object({ id: z.string(), name: z.string() }).passthrough()
|
||||
|
||||
export const definePostSelector = <TBody extends z.ZodType, TResponse extends z.ZodType>(
|
||||
path: string,
|
||||
body: TBody,
|
||||
response: TResponse
|
||||
) =>
|
||||
defineRouteContract({
|
||||
method: 'POST',
|
||||
path,
|
||||
body,
|
||||
response: { mode: 'json', schema: response },
|
||||
})
|
||||
|
||||
export const defineGetSelector = <TQuery extends z.ZodType, TResponse extends z.ZodType>(
|
||||
path: string,
|
||||
query: TQuery,
|
||||
response: TResponse
|
||||
) =>
|
||||
defineRouteContract({
|
||||
method: 'GET',
|
||||
path,
|
||||
query,
|
||||
response: { mode: 'json', schema: response },
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialWorkflowBodySchema,
|
||||
defineGetSelector,
|
||||
definePostSelector,
|
||||
fileOptionSchema,
|
||||
idDisplayNameSchema,
|
||||
optionalString,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractBody, ContractJsonResponse, ContractQuery } from '@/lib/api/contracts/types'
|
||||
|
||||
export const sharepointListsBodySchema = credentialWorkflowBodySchema.extend({
|
||||
siteId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const sharepointSitesBodySchema = credentialWorkflowBodySchema.extend({
|
||||
query: optionalString,
|
||||
})
|
||||
|
||||
export const sharepointSiteQuerySchema = z.object({
|
||||
credentialId: z.preprocess(
|
||||
(value) => value ?? '',
|
||||
z.string().min(1, 'Credential ID and Site ID are required')
|
||||
),
|
||||
siteId: z.preprocess(
|
||||
(value) => value ?? '',
|
||||
z.string().min(1, 'Credential ID and Site ID are required')
|
||||
),
|
||||
})
|
||||
|
||||
export const sharepointListsSelectorContract = definePostSelector(
|
||||
'/api/tools/sharepoint/lists',
|
||||
sharepointListsBodySchema,
|
||||
z.object({ lists: z.array(idDisplayNameSchema) })
|
||||
)
|
||||
|
||||
export const sharepointSitesSelectorContract = definePostSelector(
|
||||
'/api/tools/sharepoint/sites',
|
||||
sharepointSitesBodySchema,
|
||||
z.object({ files: z.array(fileOptionSchema) })
|
||||
)
|
||||
|
||||
export const sharepointSiteSelectorContract = defineGetSelector(
|
||||
'/api/tools/sharepoint/site',
|
||||
sharepointSiteQuerySchema,
|
||||
z.object({ site: fileOptionSchema.optional() }).passthrough()
|
||||
)
|
||||
|
||||
export type SharepointListsSelectorResponse = ContractJsonResponse<
|
||||
typeof sharepointListsSelectorContract
|
||||
>
|
||||
export type SharepointListsSelectorBody = ContractBody<typeof sharepointListsSelectorContract>
|
||||
export type SharepointSitesSelectorResponse = ContractJsonResponse<
|
||||
typeof sharepointSitesSelectorContract
|
||||
>
|
||||
export type SharepointSitesSelectorBody = ContractBody<typeof sharepointSitesSelectorContract>
|
||||
export type SharepointSiteSelectorResponse = ContractJsonResponse<
|
||||
typeof sharepointSiteSelectorContract
|
||||
>
|
||||
export type SharepointSiteSelectorQuery = ContractQuery<typeof sharepointSiteSelectorContract>
|
||||
@@ -0,0 +1,49 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialWorkflowBodySchema,
|
||||
definePostSelector,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
|
||||
const slackChannelSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
isPrivate: z.boolean(),
|
||||
})
|
||||
const slackUserSchema = z
|
||||
.object({ id: z.string(), name: z.string(), real_name: z.string() })
|
||||
.passthrough()
|
||||
|
||||
export const slackUsersBodySchema = credentialWorkflowBodySchema.extend({
|
||||
userId: z.string().optional(),
|
||||
})
|
||||
|
||||
export const slackChannelsSelectorContract = definePostSelector(
|
||||
'/api/tools/slack/channels',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ channels: z.array(slackChannelSchema) })
|
||||
)
|
||||
|
||||
export const slackUsersSelectorContract = definePostSelector(
|
||||
'/api/tools/slack/users',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ users: z.array(slackUserSchema) })
|
||||
)
|
||||
|
||||
export const slackUserSelectorContract = definePostSelector(
|
||||
'/api/tools/slack/users',
|
||||
credentialWorkflowBodySchema.extend({ userId: z.string().min(1) }),
|
||||
z.object({ user: slackUserSchema })
|
||||
)
|
||||
|
||||
export const slackUsersListOrDetailContract = definePostSelector(
|
||||
'/api/tools/slack/users',
|
||||
slackUsersBodySchema,
|
||||
z.union([z.object({ user: slackUserSchema }), z.object({ users: z.array(slackUserSchema) })])
|
||||
)
|
||||
|
||||
export type SlackChannelsSelectorResponse = ContractJsonResponse<
|
||||
typeof slackChannelsSelectorContract
|
||||
>
|
||||
export type SlackUsersSelectorResponse = ContractJsonResponse<typeof slackUsersSelectorContract>
|
||||
export type SlackUserSelectorResponse = ContractJsonResponse<typeof slackUserSelectorContract>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialWorkflowBodySchema,
|
||||
definePostSelector,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
|
||||
const trelloBoardSchema = z
|
||||
.object({ id: z.string(), name: z.string(), closed: z.boolean().optional() })
|
||||
.passthrough()
|
||||
|
||||
export const trelloBoardsSelectorContract = definePostSelector(
|
||||
'/api/tools/trello/boards',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ boards: z.array(trelloBoardSchema) })
|
||||
)
|
||||
|
||||
export type TrelloBoardsSelectorResponse = ContractJsonResponse<typeof trelloBoardsSelectorContract>
|
||||
@@ -0,0 +1,77 @@
|
||||
import { z } from 'zod'
|
||||
import { defineGetSelector, optionalString } from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
|
||||
export const WEALTHBOX_ITEM_TYPES = ['note', 'contact', 'task'] as const
|
||||
|
||||
const wealthboxItemSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
content: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
|
||||
const wealthboxItemsResponseSchema = z.object({
|
||||
items: z.array(wealthboxItemSchema),
|
||||
})
|
||||
|
||||
const wealthboxItemResponseSchema = z.object({
|
||||
item: wealthboxItemSchema.optional(),
|
||||
})
|
||||
|
||||
export const wealthboxItemsQuerySchema = z.object({
|
||||
credentialId: z.string().min(1),
|
||||
type: z.preprocess(
|
||||
(value) => (value === '' || value === undefined ? 'contact' : value),
|
||||
z.literal('contact').default('contact')
|
||||
),
|
||||
query: z.preprocess(
|
||||
(value) => (value === undefined || value === null ? '' : value),
|
||||
optionalString.default('')
|
||||
),
|
||||
})
|
||||
|
||||
export const wealthboxItemQuerySchema = z.object({
|
||||
credentialId: z.preprocess(
|
||||
(value) => value ?? '',
|
||||
z.string().min(1, 'Credential ID is required')
|
||||
),
|
||||
itemId: z.preprocess((value) => value ?? '', z.string().min(1, 'Item ID is required')),
|
||||
type: z.preprocess(
|
||||
(value) => value || 'note',
|
||||
z.enum(WEALTHBOX_ITEM_TYPES, { error: 'type must be one of: note, contact, task' })
|
||||
),
|
||||
})
|
||||
|
||||
export const wealthboxItemsSelectorContract = defineGetSelector(
|
||||
'/api/tools/wealthbox/items',
|
||||
wealthboxItemsQuerySchema,
|
||||
wealthboxItemsResponseSchema
|
||||
)
|
||||
|
||||
export const wealthboxItemContract = defineGetSelector(
|
||||
'/api/tools/wealthbox/item',
|
||||
wealthboxItemQuerySchema,
|
||||
wealthboxItemResponseSchema
|
||||
)
|
||||
|
||||
export const wealthboxOAuthItemsContract = defineGetSelector(
|
||||
'/api/auth/oauth/wealthbox/items',
|
||||
wealthboxItemsQuerySchema,
|
||||
wealthboxItemsResponseSchema
|
||||
)
|
||||
|
||||
export const wealthboxOAuthItemContract = defineGetSelector(
|
||||
'/api/auth/oauth/wealthbox/item',
|
||||
wealthboxItemQuerySchema,
|
||||
wealthboxItemResponseSchema
|
||||
)
|
||||
|
||||
export type WealthboxItemsSelectorResponse = ContractJsonResponse<
|
||||
typeof wealthboxItemsSelectorContract
|
||||
>
|
||||
export type WealthboxItemResponse = ContractJsonResponse<typeof wealthboxItemContract>
|
||||
export type WealthboxOAuthItemsResponse = ContractJsonResponse<typeof wealthboxOAuthItemsContract>
|
||||
export type WealthboxOAuthItemResponse = ContractJsonResponse<typeof wealthboxOAuthItemContract>
|
||||
@@ -0,0 +1,49 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialWorkflowBodySchema,
|
||||
definePostSelector,
|
||||
idNameSchema,
|
||||
optionalString,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
|
||||
/**
|
||||
* Webflow `/sites` accepts an optional `siteId`. When provided, the route
|
||||
* dispatches to the single-site detail endpoint instead of the list endpoint.
|
||||
*/
|
||||
export const webflowSitesBodySchema = credentialWorkflowBodySchema.extend({
|
||||
siteId: optionalString,
|
||||
})
|
||||
|
||||
export const webflowCollectionsBodySchema = credentialWorkflowBodySchema.extend({
|
||||
siteId: z.string().min(1, 'Site ID is required'),
|
||||
})
|
||||
|
||||
export const webflowItemsBodySchema = credentialWorkflowBodySchema.extend({
|
||||
collectionId: z.string().min(1, 'Collection ID is required'),
|
||||
search: optionalString,
|
||||
})
|
||||
|
||||
export const webflowSitesSelectorContract = definePostSelector(
|
||||
'/api/tools/webflow/sites',
|
||||
webflowSitesBodySchema,
|
||||
z.object({ sites: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
export const webflowCollectionsSelectorContract = definePostSelector(
|
||||
'/api/tools/webflow/collections',
|
||||
webflowCollectionsBodySchema,
|
||||
z.object({ collections: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
export const webflowItemsSelectorContract = definePostSelector(
|
||||
'/api/tools/webflow/items',
|
||||
webflowItemsBodySchema,
|
||||
z.object({ items: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
export type WebflowSitesSelectorResponse = ContractJsonResponse<typeof webflowSitesSelectorContract>
|
||||
export type WebflowCollectionsSelectorResponse = ContractJsonResponse<
|
||||
typeof webflowCollectionsSelectorContract
|
||||
>
|
||||
export type WebflowItemsSelectorResponse = ContractJsonResponse<typeof webflowItemsSelectorContract>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
credentialWorkflowBodySchema,
|
||||
definePostSelector,
|
||||
idNameSchema,
|
||||
} from '@/lib/api/contracts/selectors/shared'
|
||||
import type { ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
|
||||
export const zoomMeetingsSelectorContract = definePostSelector(
|
||||
'/api/tools/zoom/meetings',
|
||||
credentialWorkflowBodySchema,
|
||||
z.object({ meetings: z.array(idNameSchema) })
|
||||
)
|
||||
|
||||
export type ZoomMeetingsSelectorResponse = ContractJsonResponse<typeof zoomMeetingsSelectorContract>
|
||||
@@ -0,0 +1,97 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
export const skillSchema = z.object({
|
||||
id: z.string(),
|
||||
workspaceId: z.string().nullable(),
|
||||
userId: z.string().nullable(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
content: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
/** True for built-in template skills, which are read-only and not stored in the DB. */
|
||||
readOnly: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type Skill = z.output<typeof skillSchema>
|
||||
|
||||
export const skillUpsertItemSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
name: z
|
||||
.string()
|
||||
.min(1, 'Skill name is required')
|
||||
.max(64)
|
||||
.regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, 'Name must be kebab-case (e.g. my-skill)'),
|
||||
description: z.string().min(1, 'Description is required').max(1024),
|
||||
content: z.string().min(1, 'Content is required').max(50_000, 'Content is too large'),
|
||||
})
|
||||
|
||||
export const listSkillsQuerySchema = z.object({
|
||||
workspaceId: z.string().min(1),
|
||||
})
|
||||
|
||||
export const upsertSkillsBodySchema = z.object({
|
||||
skills: z.array(skillUpsertItemSchema),
|
||||
workspaceId: z.string().min(1),
|
||||
source: z.enum(['settings', 'tool_input']).optional(),
|
||||
})
|
||||
|
||||
export const deleteSkillQuerySchema = z.object({
|
||||
id: z.string().min(1),
|
||||
workspaceId: z.string().min(1),
|
||||
source: z.enum(['settings', 'tool_input']).optional(),
|
||||
})
|
||||
|
||||
export const importSkillBodySchema = z.object({
|
||||
url: z.string().url('A valid URL is required'),
|
||||
})
|
||||
|
||||
export const listSkillsContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/skills',
|
||||
query: listSkillsQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
data: z.array(skillSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const upsertSkillsContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/skills',
|
||||
body: upsertSkillsBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
data: z.array(skillSchema),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteSkillContract = defineRouteContract({
|
||||
method: 'DELETE',
|
||||
path: '/api/skills',
|
||||
query: deleteSkillQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const importSkillContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/skills/import',
|
||||
body: importSkillBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
content: z.string(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,792 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
batchPresignedUploadResponseSchema,
|
||||
presignedUploadResponseSchema,
|
||||
} from '@/lib/api/contracts/file-uploads'
|
||||
import { workspaceFileIdSchema } from '@/lib/api/contracts/primitives'
|
||||
import {
|
||||
type ContractBodyInput,
|
||||
type ContractJsonResponse,
|
||||
type ContractParamsInput,
|
||||
type ContractQueryInput,
|
||||
defineRouteContract,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import {
|
||||
FileInputSchema,
|
||||
RawFileInputArraySchema,
|
||||
RawFileInputSchema,
|
||||
} from '@/lib/uploads/utils/file-schemas'
|
||||
|
||||
const jsonResponseSchema = z.unknown()
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1)
|
||||
const value = bytes / k ** i
|
||||
return `${value.toFixed(value >= 100 || i === 0 ? 0 : 1)} ${sizes[i]}`
|
||||
}
|
||||
|
||||
const multipartPartUrlSchema = z.object({
|
||||
partNumber: z.number(),
|
||||
url: z.string(),
|
||||
blockId: z.string().optional(),
|
||||
})
|
||||
|
||||
const multipartCompletedUploadSchema = z.object({
|
||||
success: z.literal(true),
|
||||
location: z.string(),
|
||||
path: z.string(),
|
||||
key: z.string(),
|
||||
})
|
||||
|
||||
export const initiateMultipartResponseSchema = z.object({
|
||||
uploadId: z.string(),
|
||||
key: z.string(),
|
||||
uploadToken: z.string(),
|
||||
})
|
||||
|
||||
export const getMultipartPartUrlsResponseSchema = z.object({
|
||||
presignedUrls: z.array(multipartPartUrlSchema),
|
||||
})
|
||||
|
||||
export const completeMultipartResponseSchema = z.union([
|
||||
multipartCompletedUploadSchema,
|
||||
z.object({
|
||||
results: z.array(multipartCompletedUploadSchema),
|
||||
}),
|
||||
])
|
||||
|
||||
export const abortMultipartResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
})
|
||||
|
||||
export const multipartUploadResponseSchema = z.union([
|
||||
initiateMultipartResponseSchema,
|
||||
getMultipartPartUrlsResponseSchema,
|
||||
completeMultipartResponseSchema,
|
||||
abortMultipartResponseSchema,
|
||||
])
|
||||
|
||||
const connectionFields = {
|
||||
host: z.string().min(1, 'Host is required'),
|
||||
port: z.coerce.number().int().positive().default(22),
|
||||
username: z.string().min(1, 'Username is required'),
|
||||
password: z.string().nullish(),
|
||||
privateKey: z.string().nullish(),
|
||||
passphrase: z.string().nullish(),
|
||||
}
|
||||
|
||||
export function requirePasswordOrPrivateKey<S extends z.ZodType>(schema: S): S {
|
||||
return schema.refine(
|
||||
(value) => {
|
||||
const connection = value as { password?: string | null; privateKey?: string | null }
|
||||
return Boolean(connection.password || connection.privateKey)
|
||||
},
|
||||
{ message: 'Either password or privateKey must be provided' }
|
||||
) as S
|
||||
}
|
||||
|
||||
export const boxUploadBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
parentFolderId: z.string().min(1, 'Parent folder ID is required'),
|
||||
file: FileInputSchema.optional().nullable(),
|
||||
fileContent: z.string().optional().nullable(),
|
||||
fileName: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const dropboxUploadBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
path: z.string().min(1, 'Destination path is required'),
|
||||
file: FileInputSchema.optional().nullable(),
|
||||
fileContent: z.string().optional().nullable(),
|
||||
fileName: z.string().optional().nullable(),
|
||||
mode: z.enum(['add', 'overwrite']).optional().nullable(),
|
||||
autorename: z.boolean().optional().nullable(),
|
||||
mute: z.boolean().optional().nullable(),
|
||||
})
|
||||
|
||||
export const jupyterUploadBodySchema = z.object({
|
||||
serverUrl: z.string().min(1, 'Server URL is required'),
|
||||
token: z.string().min(1, 'Token is required'),
|
||||
directory: z.string().optional().nullable(),
|
||||
file: FileInputSchema.optional().nullable(),
|
||||
fileContent: z.string().optional().nullable(),
|
||||
fileName: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const wordpressUploadBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
siteId: z.string().min(1, 'Site ID is required'),
|
||||
file: RawFileInputSchema.optional().nullable(),
|
||||
filename: z.string().optional().nullable(),
|
||||
title: z.string().optional().nullable(),
|
||||
caption: z.string().optional().nullable(),
|
||||
altText: z.string().optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const sftpListBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
remotePath: z.string().min(1, 'Remote path is required'),
|
||||
detailed: z.boolean().default(false),
|
||||
})
|
||||
)
|
||||
|
||||
export const sftpDeleteBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
remotePath: z.string().min(1, 'Remote path is required'),
|
||||
recursive: z.boolean().default(false),
|
||||
})
|
||||
)
|
||||
|
||||
export const sftpMkdirBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
remotePath: z.string().min(1, 'Remote path is required'),
|
||||
recursive: z.boolean().default(false),
|
||||
})
|
||||
)
|
||||
|
||||
export const sftpDownloadBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
remotePath: z.string().min(1, 'Remote path is required'),
|
||||
encoding: z.enum(['utf-8', 'base64']).default('utf-8'),
|
||||
})
|
||||
)
|
||||
|
||||
export const sftpUploadBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
remotePath: z.string().min(1, 'Remote path is required'),
|
||||
files: RawFileInputArraySchema.optional().nullable(),
|
||||
fileContent: z.string().nullish(),
|
||||
fileName: z.string().nullish(),
|
||||
overwrite: z.boolean().default(true),
|
||||
permissions: z.string().nullish(),
|
||||
})
|
||||
)
|
||||
|
||||
export const sshCheckCommandExistsBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
commandName: z.string().min(1, 'Command name is required'),
|
||||
})
|
||||
)
|
||||
|
||||
export const sshCheckFileExistsBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
path: z.string().min(1, 'Path is required'),
|
||||
type: z.enum(['file', 'directory', 'any']).default('any'),
|
||||
})
|
||||
)
|
||||
|
||||
export const sshCreateDirectoryBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
path: z.string().min(1, 'Path is required'),
|
||||
recursive: z.boolean().default(true),
|
||||
permissions: z.string().default('0755'),
|
||||
})
|
||||
)
|
||||
|
||||
export const sshDeleteFileBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
path: z.string().min(1, 'Path is required'),
|
||||
recursive: z.boolean().default(false),
|
||||
force: z.boolean().default(false),
|
||||
})
|
||||
)
|
||||
|
||||
export const sshDownloadFileBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
remotePath: z.string().min(1, 'Remote path is required'),
|
||||
})
|
||||
)
|
||||
|
||||
export const sshExecuteCommandBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
command: z.string().min(1, 'Command is required'),
|
||||
workingDirectory: z.string().nullish(),
|
||||
})
|
||||
)
|
||||
|
||||
export const sshExecuteScriptBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
script: z.string().min(1, 'Script content is required'),
|
||||
interpreter: z.string().default('/bin/bash'),
|
||||
workingDirectory: z.string().nullish(),
|
||||
})
|
||||
)
|
||||
|
||||
export const sshGetSystemInfoBodySchema = requirePasswordOrPrivateKey(z.object(connectionFields))
|
||||
|
||||
export const sshListDirectoryBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
path: z.string().min(1, 'Path is required'),
|
||||
detailed: z.boolean().default(true),
|
||||
recursive: z.boolean().default(false),
|
||||
})
|
||||
)
|
||||
|
||||
export const sshMoveRenameBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
sourcePath: z.string().min(1, 'Source path is required'),
|
||||
destinationPath: z.string().min(1, 'Destination path is required'),
|
||||
overwrite: z.boolean().default(false),
|
||||
})
|
||||
)
|
||||
|
||||
export const sshReadFileContentBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
path: z.string().min(1, 'Path is required'),
|
||||
encoding: z.string().default('utf-8'),
|
||||
maxSize: z.coerce.number().min(0.01).max(50).default(10),
|
||||
})
|
||||
)
|
||||
|
||||
export const sshUploadFileBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
fileContent: z.string().min(1, 'File content is required'),
|
||||
fileName: z.string().min(1, 'File name is required'),
|
||||
remotePath: z.string().min(1, 'Remote path is required'),
|
||||
permissions: z.string().nullish(),
|
||||
overwrite: z.boolean().default(true),
|
||||
})
|
||||
)
|
||||
|
||||
export const sshWriteFileContentBodySchema = requirePasswordOrPrivateKey(
|
||||
z.object({
|
||||
...connectionFields,
|
||||
path: z.string().min(1, 'Path is required'),
|
||||
content: z.string(),
|
||||
mode: z.enum(['overwrite', 'append', 'create']).default('overwrite'),
|
||||
permissions: z.string().nullish(),
|
||||
})
|
||||
)
|
||||
|
||||
export const storageContextSchema = z.enum([
|
||||
'knowledge-base',
|
||||
'chat',
|
||||
'copilot',
|
||||
'mothership',
|
||||
'execution',
|
||||
'workspace',
|
||||
'profile-pictures',
|
||||
'og-images',
|
||||
'logs',
|
||||
'workspace-logos',
|
||||
])
|
||||
|
||||
export const fileDownloadBodySchema = z
|
||||
.object({
|
||||
key: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
url: z
|
||||
.string()
|
||||
.url()
|
||||
.refine((value) => ['http:', 'https:'].includes(new URL(value).protocol), {
|
||||
message: 'URL must use http or https',
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const fileParseBodySchema = z
|
||||
.object({
|
||||
filePath: z
|
||||
.union([z.string(), z.array(z.string()).max(10, 'At most 10 files can be parsed at once')])
|
||||
.optional(),
|
||||
fileType: z.string().optional().default(''),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
workspaceId: z.string().optional().default(''),
|
||||
workflowId: z.string().optional(),
|
||||
executionId: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const fileDeleteBodySchema = z
|
||||
.object({
|
||||
filePath: z.string().optional(),
|
||||
context: storageContextSchema.optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const MAX_FILE_SIZE = 100 * 1024 * 1024
|
||||
export const validUploadTypes = [
|
||||
'knowledge-base',
|
||||
'chat',
|
||||
'copilot',
|
||||
'profile-pictures',
|
||||
'mothership',
|
||||
'workspace-logos',
|
||||
'execution',
|
||||
] as const
|
||||
|
||||
export const uploadTypeSchema = z.enum(validUploadTypes)
|
||||
|
||||
export const presignedUploadQuerySchema = z.object({
|
||||
type: uploadTypeSchema,
|
||||
})
|
||||
|
||||
export const presignedUrlBodySchema = z
|
||||
.object({
|
||||
fileName: z
|
||||
.string({ error: 'fileName is required and cannot be empty' })
|
||||
.refine((value) => value.trim().length > 0, {
|
||||
message: 'fileName is required and cannot be empty',
|
||||
}),
|
||||
contentType: z
|
||||
.string({ error: 'contentType is required and cannot be empty' })
|
||||
.refine((value) => value.trim().length > 0, {
|
||||
message: 'contentType is required and cannot be empty',
|
||||
}),
|
||||
fileSize: z
|
||||
.number({ error: 'fileSize must be a positive number' })
|
||||
.positive('fileSize must be a positive number')
|
||||
.superRefine((val, ctx) => {
|
||||
if (val > MAX_FILE_SIZE) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: `File size ${formatFileSize(val)} exceeds maximum allowed size of ${formatFileSize(MAX_FILE_SIZE)}`,
|
||||
})
|
||||
}
|
||||
}),
|
||||
userId: z.string().optional(),
|
||||
chatId: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const batchPresignedUrlBodySchema = z
|
||||
.object({
|
||||
files: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
fileName: z.string().refine((value) => value.trim().length > 0, {
|
||||
message: 'fileName is required for all files',
|
||||
}),
|
||||
contentType: z.string().refine((value) => value.trim().length > 0, {
|
||||
message: 'contentType is required for all files',
|
||||
}),
|
||||
fileSize: z.number(),
|
||||
})
|
||||
.passthrough()
|
||||
.superRefine((file, ctx) => {
|
||||
const name = typeof file.fileName === 'string' ? file.fileName : 'file'
|
||||
if (!Number.isFinite(file.fileSize) || file.fileSize <= 0) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['fileSize'],
|
||||
message: `${name} is empty (fileSize must be greater than 0)`,
|
||||
})
|
||||
} else if (file.fileSize > MAX_FILE_SIZE) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['fileSize'],
|
||||
message: `${name} (${formatFileSize(file.fileSize)}) exceeds maximum allowed size of ${formatFileSize(MAX_FILE_SIZE)}`,
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
.min(1, 'files array is required and cannot be empty')
|
||||
.max(100, 'Cannot process more than 100 files at once'),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const multipartActionSchema = z.enum(['initiate', 'get-part-urls', 'complete', 'abort'])
|
||||
|
||||
export const initiateMultipartBodySchema = z
|
||||
.object({
|
||||
fileName: z.string(),
|
||||
contentType: z.string(),
|
||||
fileSize: z.number(),
|
||||
workspaceId: z.string({ error: 'workspaceId is required' }).min(1, 'workspaceId is required'),
|
||||
context: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const tokenBoundMultipartBodySchema = z
|
||||
.object({
|
||||
uploadToken: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const getMultipartPartUrlsBodySchema = tokenBoundMultipartBodySchema.extend({
|
||||
partNumbers: z.array(z.number()),
|
||||
})
|
||||
|
||||
export const completeMultipartBodySchema = z
|
||||
.object({
|
||||
uploadToken: z.string().optional(),
|
||||
parts: z.unknown().optional(),
|
||||
uploads: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
uploadToken: z.string().optional(),
|
||||
parts: z.unknown().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export type CompleteMultipartBody = z.output<typeof completeMultipartBodySchema>
|
||||
|
||||
export const uploadFilesFormFilesSchema = z.preprocess(
|
||||
(value) => (Array.isArray(value) ? value.filter((entry) => entry instanceof File) : value),
|
||||
z.array(z.custom<File>((value) => value instanceof File)).min(1, 'No files provided')
|
||||
)
|
||||
|
||||
export const uploadFilesFormFieldsSchema = z.object({
|
||||
workflowId: z.string().nullable(),
|
||||
executionId: z.string().nullable(),
|
||||
workspaceId: z.string().nullable(),
|
||||
context: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const fileServeParamsSchema = z.object({
|
||||
path: z.array(z.string()).min(1),
|
||||
})
|
||||
|
||||
export const fileServeQuerySchema = z.object({
|
||||
raw: z.string().nullish(),
|
||||
/** Content version (the file record's `updatedAt`). Present => the URL is content-immutable and may be cached indefinitely by the browser. */
|
||||
v: z.string().nullish(),
|
||||
})
|
||||
|
||||
export const fileViewParamsSchema = z.object({
|
||||
id: workspaceFileIdSchema,
|
||||
})
|
||||
|
||||
export const fileExportParamsSchema = z.object({
|
||||
id: workspaceFileIdSchema,
|
||||
})
|
||||
|
||||
export const boxUploadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/box/upload',
|
||||
body: boxUploadBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const dropboxUploadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/dropbox/upload',
|
||||
body: dropboxUploadBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const jupyterUploadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/jupyter/upload',
|
||||
body: jupyterUploadBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const wordpressUploadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/wordpress/upload',
|
||||
body: wordpressUploadBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sftpListContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/sftp/list',
|
||||
body: sftpListBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sftpDeleteContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/sftp/delete',
|
||||
body: sftpDeleteBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sftpMkdirContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/sftp/mkdir',
|
||||
body: sftpMkdirBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sftpDownloadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/sftp/download',
|
||||
body: sftpDownloadBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sftpUploadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/sftp/upload',
|
||||
body: sftpUploadBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sshCheckCommandExistsContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/ssh/check-command-exists',
|
||||
body: sshCheckCommandExistsBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sshCheckFileExistsContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/ssh/check-file-exists',
|
||||
body: sshCheckFileExistsBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sshCreateDirectoryContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/ssh/create-directory',
|
||||
body: sshCreateDirectoryBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sshDeleteFileContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/ssh/delete-file',
|
||||
body: sshDeleteFileBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sshDownloadFileContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/ssh/download-file',
|
||||
body: sshDownloadFileBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sshExecuteCommandContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/ssh/execute-command',
|
||||
body: sshExecuteCommandBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sshExecuteScriptContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/ssh/execute-script',
|
||||
body: sshExecuteScriptBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sshGetSystemInfoContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/ssh/get-system-info',
|
||||
body: sshGetSystemInfoBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sshListDirectoryContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/ssh/list-directory',
|
||||
body: sshListDirectoryBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sshMoveRenameContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/ssh/move-rename',
|
||||
body: sshMoveRenameBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sshReadFileContentContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/ssh/read-file-content',
|
||||
body: sshReadFileContentBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sshUploadFileContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/ssh/upload-file',
|
||||
body: sshUploadFileBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sshWriteFileContentContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/ssh/write-file-content',
|
||||
body: sshWriteFileContentBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const fileDownloadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/download',
|
||||
body: fileDownloadBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const fileParseContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/parse',
|
||||
body: fileParseBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const fileDeleteContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/delete',
|
||||
body: fileDeleteBodySchema,
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const fileUploadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/upload',
|
||||
response: { mode: 'json', schema: jsonResponseSchema },
|
||||
})
|
||||
|
||||
export const presignedUploadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/presigned',
|
||||
query: presignedUploadQuerySchema,
|
||||
body: presignedUrlBodySchema,
|
||||
response: { mode: 'json', schema: presignedUploadResponseSchema },
|
||||
})
|
||||
|
||||
export const presignedUploadBodyContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/presigned',
|
||||
body: presignedUrlBodySchema,
|
||||
response: { mode: 'json', schema: presignedUploadResponseSchema },
|
||||
})
|
||||
|
||||
export const batchPresignedUploadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/presigned/batch',
|
||||
query: presignedUploadQuerySchema,
|
||||
body: batchPresignedUrlBodySchema,
|
||||
response: { mode: 'json', schema: batchPresignedUploadResponseSchema },
|
||||
})
|
||||
|
||||
export const batchPresignedUploadBodyContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/presigned/batch',
|
||||
body: batchPresignedUrlBodySchema,
|
||||
response: { mode: 'json', schema: batchPresignedUploadResponseSchema },
|
||||
})
|
||||
|
||||
export const multipartUploadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/multipart',
|
||||
response: { mode: 'json', schema: multipartUploadResponseSchema },
|
||||
})
|
||||
|
||||
export const initiateMultipartUploadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/multipart',
|
||||
body: initiateMultipartBodySchema,
|
||||
response: { mode: 'json', schema: initiateMultipartResponseSchema },
|
||||
})
|
||||
|
||||
export const getMultipartPartUrlsContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/multipart',
|
||||
body: getMultipartPartUrlsBodySchema,
|
||||
response: { mode: 'json', schema: getMultipartPartUrlsResponseSchema },
|
||||
})
|
||||
|
||||
export const completeMultipartUploadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/multipart',
|
||||
body: completeMultipartBodySchema,
|
||||
response: { mode: 'json', schema: completeMultipartResponseSchema },
|
||||
})
|
||||
|
||||
export const abortMultipartUploadContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/files/multipart',
|
||||
body: tokenBoundMultipartBodySchema,
|
||||
response: { mode: 'json', schema: abortMultipartResponseSchema },
|
||||
})
|
||||
|
||||
export const fileServeContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/files/serve/[...path]',
|
||||
params: fileServeParamsSchema,
|
||||
query: fileServeQuerySchema,
|
||||
response: { mode: 'binary' },
|
||||
})
|
||||
|
||||
export const fileViewContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/files/view/[id]',
|
||||
params: fileViewParamsSchema,
|
||||
response: { mode: 'binary' },
|
||||
})
|
||||
|
||||
export const fileExportContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/files/export/[id]',
|
||||
params: fileExportParamsSchema,
|
||||
response: { mode: 'binary' },
|
||||
})
|
||||
|
||||
export type BoxUploadBody = ContractBodyInput<typeof boxUploadContract>
|
||||
export type BoxUploadResponse = ContractJsonResponse<typeof boxUploadContract>
|
||||
export type DropboxUploadBody = ContractBodyInput<typeof dropboxUploadContract>
|
||||
export type DropboxUploadResponse = ContractJsonResponse<typeof dropboxUploadContract>
|
||||
export type JupyterUploadBody = ContractBodyInput<typeof jupyterUploadContract>
|
||||
export type JupyterUploadResponse = ContractJsonResponse<typeof jupyterUploadContract>
|
||||
export type WordPressUploadBody = ContractBodyInput<typeof wordpressUploadContract>
|
||||
export type WordPressUploadResponse = ContractJsonResponse<typeof wordpressUploadContract>
|
||||
export type SftpDownloadBody = ContractBodyInput<typeof sftpDownloadContract>
|
||||
export type SftpUploadBody = ContractBodyInput<typeof sftpUploadContract>
|
||||
export type SftpDeleteBody = ContractBodyInput<typeof sftpDeleteContract>
|
||||
export type SftpMkdirBody = ContractBodyInput<typeof sftpMkdirContract>
|
||||
export type SshCheckCommandExistsBody = ContractBodyInput<typeof sshCheckCommandExistsContract>
|
||||
export type SshCheckFileExistsBody = ContractBodyInput<typeof sshCheckFileExistsContract>
|
||||
export type SshCreateDirectoryBody = ContractBodyInput<typeof sshCreateDirectoryContract>
|
||||
export type SshDeleteFileBody = ContractBodyInput<typeof sshDeleteFileContract>
|
||||
export type SshDownloadFileBody = ContractBodyInput<typeof sshDownloadFileContract>
|
||||
export type SshExecuteCommandBody = ContractBodyInput<typeof sshExecuteCommandContract>
|
||||
export type SshExecuteScriptBody = ContractBodyInput<typeof sshExecuteScriptContract>
|
||||
export type SshGetSystemInfoBody = ContractBodyInput<typeof sshGetSystemInfoContract>
|
||||
export type SshListDirectoryBody = ContractBodyInput<typeof sshListDirectoryContract>
|
||||
export type SshMoveRenameBody = ContractBodyInput<typeof sshMoveRenameContract>
|
||||
export type SshReadFileContentBody = ContractBodyInput<typeof sshReadFileContentContract>
|
||||
export type SshUploadFileBody = ContractBodyInput<typeof sshUploadFileContract>
|
||||
export type SshWriteFileContentBody = ContractBodyInput<typeof sshWriteFileContentContract>
|
||||
export type FileDownloadBody = ContractBodyInput<typeof fileDownloadContract>
|
||||
export type FileDownloadResponse = ContractJsonResponse<typeof fileDownloadContract>
|
||||
export type FileParseBody = ContractBodyInput<typeof fileParseContract>
|
||||
export type FileParseResponse = ContractJsonResponse<typeof fileParseContract>
|
||||
export type FileDeleteBody = ContractBodyInput<typeof fileDeleteContract>
|
||||
export type FileDeleteResponse = ContractJsonResponse<typeof fileDeleteContract>
|
||||
export type PresignedUploadQuery = ContractQueryInput<typeof presignedUploadContract>
|
||||
export type PresignedUploadBody = ContractBodyInput<typeof presignedUploadContract>
|
||||
export type PresignedUploadResponse = ContractJsonResponse<typeof presignedUploadContract>
|
||||
export type BatchPresignedUploadQuery = ContractQueryInput<typeof batchPresignedUploadContract>
|
||||
export type BatchPresignedUploadBody = ContractBodyInput<typeof batchPresignedUploadContract>
|
||||
export type BatchPresignedUploadResponse = ContractJsonResponse<typeof batchPresignedUploadContract>
|
||||
export type MultipartAction = z.output<typeof multipartActionSchema>
|
||||
export type InitiateMultipartBody = z.output<typeof initiateMultipartBodySchema>
|
||||
export type TokenBoundMultipartBody = z.output<typeof tokenBoundMultipartBodySchema>
|
||||
export type GetMultipartPartUrlsBody = z.output<typeof getMultipartPartUrlsBodySchema>
|
||||
export type FileServeParams = ContractParamsInput<typeof fileServeContract>
|
||||
export type FileServeQuery = ContractQueryInput<typeof fileServeContract>
|
||||
export type FileViewParams = ContractParamsInput<typeof fileViewContract>
|
||||
export type FileExportParams = ContractParamsInput<typeof fileExportContract>
|
||||
@@ -0,0 +1,387 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const booleanQueryParamSchema = z
|
||||
.preprocess((value) => {
|
||||
if (value === 'true') return true
|
||||
if (value === 'false') return false
|
||||
return value
|
||||
}, z.boolean())
|
||||
.optional()
|
||||
.default(false)
|
||||
|
||||
export const billingUpdateCostBodySchema = z.object({
|
||||
userId: z.string().min(1, 'User ID is required'),
|
||||
cost: z.number().min(0, 'Cost must be a non-negative number'),
|
||||
model: z.string().min(1, 'Model is required'),
|
||||
inputTokens: z.number().min(0).default(0),
|
||||
outputTokens: z.number().min(0).default(0),
|
||||
source: z
|
||||
.enum(['copilot', 'workspace-chat', 'mcp_copilot', 'mothership_block'])
|
||||
.default('copilot'),
|
||||
idempotencyKey: z.string().min(1).optional(),
|
||||
/**
|
||||
* Originating workspace, used for org-workspace cost attribution on hosted
|
||||
* Sim. Best-effort by design: self-hosted and headless clients bill through
|
||||
* this endpoint with workspace IDs that exist only in their own deployment
|
||||
* (or with none at all — the Go client omits the field when empty), so the
|
||||
* value is optional and the route only stamps it onto the ledger when it
|
||||
* resolves to a workspace in this deployment. Billing is keyed on the
|
||||
* user's billing entity and must never fail over attribution metadata.
|
||||
*/
|
||||
workspaceId: z.string().min(1).optional(),
|
||||
})
|
||||
export type BillingUpdateCostBody = z.input<typeof billingUpdateCostBodySchema>
|
||||
|
||||
export const billingSwitchPlanBodySchema = z.object({
|
||||
targetPlanName: z.string(),
|
||||
interval: z.enum(['month', 'year']).optional(),
|
||||
})
|
||||
export type BillingSwitchPlanBody = z.input<typeof billingSwitchPlanBodySchema>
|
||||
|
||||
export const billingQuerySchema = z.object({
|
||||
context: z.enum(['user', 'organization']).optional().default('user'),
|
||||
id: z.string().min(1).optional(),
|
||||
includeOrg: booleanQueryParamSchema,
|
||||
})
|
||||
|
||||
export const billingUsageDataSchema = z
|
||||
.object({
|
||||
current: z.number(),
|
||||
limit: z.number(),
|
||||
percentUsed: z.number(),
|
||||
isWarning: z.boolean(),
|
||||
isExceeded: z.boolean(),
|
||||
billingPeriodStart: z.string().nullable(),
|
||||
billingPeriodEnd: z.string().nullable(),
|
||||
lastPeriodCost: z.number(),
|
||||
lastPeriodCopilotCost: z.number(),
|
||||
daysRemaining: z.number(),
|
||||
copilotCost: z.number(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const subscriptionBillingDataSchema = z
|
||||
.object({
|
||||
type: z.enum(['individual', 'organization']),
|
||||
plan: z.string(),
|
||||
currentUsage: z.number(),
|
||||
usageLimit: z.number(),
|
||||
percentUsed: z.number(),
|
||||
isWarning: z.boolean(),
|
||||
isExceeded: z.boolean(),
|
||||
daysRemaining: z.number(),
|
||||
creditBalance: z.number(),
|
||||
billingInterval: z.enum(['month', 'year']),
|
||||
isPaid: z.boolean(),
|
||||
isPro: z.boolean(),
|
||||
isTeam: z.boolean(),
|
||||
isEnterprise: z.boolean(),
|
||||
isOrgScoped: z.boolean(),
|
||||
organizationId: z.string().nullable(),
|
||||
status: z.string().nullable(),
|
||||
seats: z.number().nullable(),
|
||||
metadata: z.unknown().nullable(),
|
||||
stripeSubscriptionId: z.string().nullable(),
|
||||
periodEnd: z.string().nullable(),
|
||||
cancelAtPeriodEnd: z.boolean().optional(),
|
||||
usage: billingUsageDataSchema,
|
||||
billingBlocked: z.boolean().optional(),
|
||||
billingBlockedReason: z.enum(['payment_failed', 'dispute']).nullable().optional(),
|
||||
blockedByOrgOwner: z.boolean().optional(),
|
||||
organization: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
role: z.enum(['owner', 'admin', 'member']),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const subscriptionApiResponseSchema = z
|
||||
.object({
|
||||
success: z.boolean(),
|
||||
context: z.string(),
|
||||
data: subscriptionBillingDataSchema,
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const organizationBillingMemberSchema = z
|
||||
.object({
|
||||
id: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
userName: z.string().nullable().optional(),
|
||||
userEmail: z.string().nullable().optional(),
|
||||
joinedAt: z.string().nullable().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const organizationBillingDataSchema = z
|
||||
.object({
|
||||
organizationId: z.string(),
|
||||
organizationName: z.string(),
|
||||
subscriptionPlan: z.string(),
|
||||
subscriptionStatus: z.string().nullable(),
|
||||
totalSeats: z.number(),
|
||||
usedSeats: z.number(),
|
||||
seatsCount: z.number(),
|
||||
totalCurrentUsage: z.number(),
|
||||
totalUsageLimit: z.number(),
|
||||
minimumBillingAmount: z.number(),
|
||||
averageUsagePerMember: z.number(),
|
||||
billingPeriodStart: z.string().nullable(),
|
||||
billingPeriodEnd: z.string().nullable(),
|
||||
members: z.array(organizationBillingMemberSchema),
|
||||
billingBlocked: z.boolean().optional(),
|
||||
billingBlockedReason: z.enum(['payment_failed', 'dispute']).nullable().optional(),
|
||||
blockedByOrgOwner: z.boolean().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const organizationBillingApiResponseSchema = z
|
||||
.object({
|
||||
success: z.boolean(),
|
||||
context: z.literal('organization'),
|
||||
data: organizationBillingDataSchema,
|
||||
userRole: z.enum(['owner', 'admin', 'member']),
|
||||
billingBlocked: z.boolean().optional(),
|
||||
billingBlockedReason: z.enum(['payment_failed', 'dispute']).nullable().optional(),
|
||||
blockedByOrgOwner: z.boolean().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const usageLimitDataSchema = z
|
||||
.object({
|
||||
currentLimit: z.number(),
|
||||
canEdit: z.boolean(),
|
||||
minimumLimit: z.number(),
|
||||
plan: z.string(),
|
||||
updatedAt: z.string().nullable(),
|
||||
scope: z.enum(['user', 'organization']),
|
||||
organizationId: z.string().nullable(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const usageQuerySchema = z.object({
|
||||
context: z.enum(['user', 'organization']).optional().default('user'),
|
||||
userId: z.string().optional(),
|
||||
organizationId: z.string().optional(),
|
||||
})
|
||||
|
||||
export const updateUsageLimitBodySchema = z
|
||||
.object({
|
||||
limit: z.number().min(0, 'Limit must be a non-negative number'),
|
||||
context: z.enum(['user', 'organization']).optional().default('user'),
|
||||
organizationId: z.string().optional(),
|
||||
})
|
||||
.refine((data) => data.context !== 'organization' || data.organizationId, {
|
||||
message: 'Organization ID is required when context is organization',
|
||||
})
|
||||
|
||||
export const usageLimitApiResponseSchema = z
|
||||
.object({
|
||||
success: z.boolean(),
|
||||
context: z.string(),
|
||||
userId: z.string(),
|
||||
organizationId: z.string().nullable(),
|
||||
data: usageLimitDataSchema,
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const organizationUsageLimitApiResponseSchema = z
|
||||
.object({
|
||||
success: z.boolean(),
|
||||
context: z.literal('organization'),
|
||||
userId: z.string(),
|
||||
organizationId: z.string(),
|
||||
data: organizationBillingDataSchema.nullable(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const purchaseCreditsBodySchema = z.object({
|
||||
amount: z.number().min(10).max(1000),
|
||||
requestId: z.string().uuid(),
|
||||
})
|
||||
|
||||
export const billingPortalBodySchema = z.object({
|
||||
context: z.enum(['user', 'organization']).optional().default('user'),
|
||||
organizationId: z.string().min(1).optional(),
|
||||
returnUrl: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
export const invoicesQuerySchema = z.object({
|
||||
context: z.enum(['user', 'organization']).optional().default('user'),
|
||||
organizationId: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
export const invoiceItemSchema = z.object({
|
||||
id: z.string(),
|
||||
number: z.string().nullable(),
|
||||
/** Invoice creation time as a Unix timestamp in seconds. */
|
||||
created: z.number(),
|
||||
/** Invoice total in the currency's minor units (e.g. cents). */
|
||||
total: z.number(),
|
||||
/** Amount paid in the currency's minor units (e.g. cents). */
|
||||
amountPaid: z.number(),
|
||||
currency: z.string(),
|
||||
status: z.string().nullable(),
|
||||
hostedInvoiceUrl: z.string().nullable(),
|
||||
invoicePdf: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const invoicesApiResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
invoices: z.array(invoiceItemSchema),
|
||||
/** True when Stripe has more invoices than the returned page (overflow). */
|
||||
hasMore: z.boolean(),
|
||||
})
|
||||
|
||||
const successResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
})
|
||||
|
||||
export const getBillingContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/billing',
|
||||
query: billingQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.union([subscriptionApiResponseSchema, organizationBillingApiResponseSchema]),
|
||||
},
|
||||
})
|
||||
|
||||
export const getUserBillingContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/billing',
|
||||
query: billingQuerySchema.extend({
|
||||
context: z.literal('user').optional().default('user'),
|
||||
}),
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: subscriptionApiResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getOrganizationBillingContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/billing',
|
||||
query: billingQuerySchema.extend({
|
||||
context: z.literal('organization'),
|
||||
id: z.string().min(1),
|
||||
}),
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: organizationBillingApiResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const getUsageLimitContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/usage',
|
||||
query: usageQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.union([usageLimitApiResponseSchema, organizationUsageLimitApiResponseSchema]),
|
||||
},
|
||||
})
|
||||
|
||||
export const getUserUsageLimitContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/usage',
|
||||
query: usageQuerySchema.extend({
|
||||
context: z.literal('user').optional().default('user'),
|
||||
}),
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: usageLimitApiResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateUsageLimitContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/usage',
|
||||
body: updateUsageLimitBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.union([usageLimitApiResponseSchema, organizationUsageLimitApiResponseSchema]),
|
||||
},
|
||||
})
|
||||
|
||||
export const purchaseCreditsContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/billing/credits',
|
||||
body: purchaseCreditsBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: successResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const createBillingPortalContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/billing/portal',
|
||||
body: billingPortalBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
url: z.string().min(1),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const getInvoicesContract = defineRouteContract({
|
||||
method: 'GET',
|
||||
path: '/api/billing/invoices',
|
||||
query: invoicesQuerySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: invoicesApiResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const billingSwitchPlanResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
plan: z.string().optional(),
|
||||
interval: z.enum(['month', 'year']).optional(),
|
||||
message: z.string().optional(),
|
||||
})
|
||||
|
||||
export const billingUpdateCostResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
message: z.string().optional(),
|
||||
data: z.object({
|
||||
userId: z.string().optional(),
|
||||
cost: z.number().optional(),
|
||||
billingEnabled: z.boolean().optional(),
|
||||
processedAt: z.string(),
|
||||
requestId: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
export const billingSwitchPlanContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/billing/switch-plan',
|
||||
body: billingSwitchPlanBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: billingSwitchPlanResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const billingUpdateCostContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/billing/update-cost',
|
||||
body: billingUpdateCostBodySchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: billingUpdateCostResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export type BillingUsageData = z.infer<typeof billingUsageDataSchema>
|
||||
export type SubscriptionBillingData = z.infer<typeof subscriptionBillingDataSchema>
|
||||
export type SubscriptionApiResponse = z.infer<typeof subscriptionApiResponseSchema>
|
||||
export type OrganizationBillingApiResponse = z.infer<typeof organizationBillingApiResponseSchema>
|
||||
export type UsageLimitApiResponse = z.infer<typeof usageLimitApiResponseSchema>
|
||||
export type InvoiceItem = z.infer<typeof invoiceItemSchema>
|
||||
export type InvoicesApiResponse = z.infer<typeof invoicesApiResponseSchema>
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { tableEventStreamQuerySchema } from '@/lib/api/contracts/tables'
|
||||
|
||||
describe('tableEventStreamQuerySchema', () => {
|
||||
it('parses an explicit cursor', () => {
|
||||
expect(tableEventStreamQuerySchema.parse({ from: '7' })).toEqual({ from: 7 })
|
||||
})
|
||||
|
||||
it('keeps 0 as an explicit replay-from-start cursor', () => {
|
||||
expect(tableEventStreamQuerySchema.parse({ from: '0' })).toEqual({ from: 0 })
|
||||
})
|
||||
|
||||
it('yields undefined when absent — the tail-from-latest signal', () => {
|
||||
expect(tableEventStreamQuerySchema.parse({})).toEqual({ from: undefined })
|
||||
})
|
||||
|
||||
it('yields undefined for invalid values instead of coercing to a full replay', () => {
|
||||
expect(tableEventStreamQuerySchema.parse({ from: 'abc' })).toEqual({ from: undefined })
|
||||
expect(tableEventStreamQuerySchema.parse({ from: '-4' })).toEqual({ from: undefined })
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
||||
import { z } from 'zod'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const ALLOWED_TELEMETRY_CATEGORIES = [
|
||||
'page_view',
|
||||
'feature_usage',
|
||||
'performance',
|
||||
'error',
|
||||
'workflow',
|
||||
'consent',
|
||||
'batch',
|
||||
] as const
|
||||
|
||||
export const telemetryCategorySchema = z.enum([...ALLOWED_TELEMETRY_CATEGORIES] as [
|
||||
string,
|
||||
...string[],
|
||||
])
|
||||
export type TelemetryCategory = z.output<typeof telemetryCategorySchema>
|
||||
|
||||
const SENSITIVE_PATTERNS = [/password/, /token/, /secret/, /key/, /auth/, /credential/, /private/]
|
||||
|
||||
export const telemetryEventSchema = z
|
||||
.object({
|
||||
category: telemetryCategorySchema,
|
||||
action: z.string().min(1),
|
||||
})
|
||||
.passthrough()
|
||||
.refine(
|
||||
(data) => {
|
||||
const jsonStr = JSON.stringify(data).toLowerCase()
|
||||
return !SENSITIVE_PATTERNS.some((pattern) => pattern.test(jsonStr))
|
||||
},
|
||||
{ message: 'Telemetry data contains sensitive information' }
|
||||
)
|
||||
export type TelemetryEvent = z.output<typeof telemetryEventSchema>
|
||||
|
||||
export const telemetryContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/telemetry',
|
||||
body: telemetryEventSchema,
|
||||
response: {
|
||||
mode: 'json',
|
||||
schema: z.object({
|
||||
success: z.literal(true),
|
||||
forwarded: z.boolean(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { tiktokPublishVideoBodySchema } from '@/lib/api/contracts/tiktok-tools'
|
||||
|
||||
const file = {
|
||||
key: 'workspace/test/video.mp4',
|
||||
name: 'video.mp4',
|
||||
size: 1024,
|
||||
type: 'video/mp4',
|
||||
}
|
||||
|
||||
describe('tiktokPublishVideoBodySchema', () => {
|
||||
it('accepts a draft without direct-post metadata', () => {
|
||||
expect(
|
||||
tiktokPublishVideoBodySchema.safeParse({
|
||||
accessToken: 'token',
|
||||
mode: 'draft',
|
||||
file,
|
||||
}).success
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('requires direct-post privacy and commercial-content disclosure', () => {
|
||||
expect(
|
||||
tiktokPublishVideoBodySchema.safeParse({
|
||||
accessToken: 'token',
|
||||
mode: 'direct',
|
||||
file,
|
||||
postInfo: { privacy_level: 'SELF_ONLY' },
|
||||
}).success
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts documented direct-post metadata', () => {
|
||||
expect(
|
||||
tiktokPublishVideoBodySchema.safeParse({
|
||||
accessToken: 'token',
|
||||
mode: 'direct',
|
||||
file,
|
||||
musicUsageConsent: 'accepted',
|
||||
postInfo: {
|
||||
title: 'A test video',
|
||||
privacy_level: 'SELF_ONLY',
|
||||
disable_duet: true,
|
||||
disable_stitch: true,
|
||||
disable_comment: true,
|
||||
brand_content_toggle: false,
|
||||
},
|
||||
}).success
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects direct posting without explicit music usage consent', () => {
|
||||
expect(
|
||||
tiktokPublishVideoBodySchema.safeParse({
|
||||
accessToken: 'token',
|
||||
mode: 'direct',
|
||||
file,
|
||||
postInfo: {
|
||||
privacy_level: 'SELF_ONLY',
|
||||
disable_duet: true,
|
||||
disable_stitch: true,
|
||||
disable_comment: true,
|
||||
brand_content_toggle: false,
|
||||
},
|
||||
}).success
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects an undocumented privacy value', () => {
|
||||
expect(
|
||||
tiktokPublishVideoBodySchema.safeParse({
|
||||
accessToken: 'token',
|
||||
mode: 'direct',
|
||||
file,
|
||||
musicUsageConsent: 'accepted',
|
||||
postInfo: {
|
||||
privacy_level: 'PRIVATE',
|
||||
disable_duet: true,
|
||||
disable_stitch: true,
|
||||
disable_comment: true,
|
||||
brand_content_toggle: false,
|
||||
},
|
||||
}).success
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
type ContractBodyInput,
|
||||
type ContractJsonResponse,
|
||||
defineRouteContract,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'
|
||||
|
||||
const tiktokPublishVideoPostInfoSchema = z.object({
|
||||
title: z.string().max(2200).optional(),
|
||||
privacy_level: z.enum([
|
||||
'PUBLIC_TO_EVERYONE',
|
||||
'MUTUAL_FOLLOW_FRIENDS',
|
||||
'FOLLOWER_OF_CREATOR',
|
||||
'SELF_ONLY',
|
||||
]),
|
||||
disable_duet: z.boolean(),
|
||||
disable_stitch: z.boolean(),
|
||||
disable_comment: z.boolean(),
|
||||
video_cover_timestamp_ms: z.number().int().nonnegative().optional(),
|
||||
is_aigc: z.boolean().optional(),
|
||||
brand_content_toggle: z.boolean(),
|
||||
brand_organic_toggle: z.boolean().optional(),
|
||||
})
|
||||
|
||||
const tiktokPublishVideoBaseSchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
file: RawFileInputSchema,
|
||||
})
|
||||
|
||||
export const tiktokPublishVideoBodySchema = z.discriminatedUnion('mode', [
|
||||
tiktokPublishVideoBaseSchema.extend({
|
||||
mode: z.literal('direct'),
|
||||
postInfo: tiktokPublishVideoPostInfoSchema,
|
||||
musicUsageConsent: z.literal('accepted'),
|
||||
}),
|
||||
tiktokPublishVideoBaseSchema.extend({
|
||||
mode: z.literal('draft'),
|
||||
}),
|
||||
])
|
||||
|
||||
export const tiktokPublishVideoResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
output: z.object({ publishId: z.string() }).optional(),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
|
||||
export const tiktokPublishVideoContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/tiktok/publish-video',
|
||||
body: tiktokPublishVideoBodySchema,
|
||||
response: { mode: 'json', schema: tiktokPublishVideoResponseSchema },
|
||||
})
|
||||
|
||||
export type TikTokPublishVideoBody = ContractBodyInput<typeof tiktokPublishVideoContract>
|
||||
export type TikTokPublishVideoResponse = ContractJsonResponse<typeof tiktokPublishVideoContract>
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const toolFailureResponseSchema = z.object({
|
||||
success: z.literal(false),
|
||||
error: z.string(),
|
||||
details: z.array(z.unknown()).optional(),
|
||||
})
|
||||
|
||||
export const toolSuccessResponseSchema = <S extends z.ZodType>(output: S) =>
|
||||
z.object({
|
||||
success: z.literal(true),
|
||||
output,
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import { z } from 'zod'
|
||||
import { genericToolResponseSchema } from '@/lib/api/contracts/tools/shared'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'
|
||||
|
||||
const a2aBaseBodySchema = z.object({
|
||||
agentUrl: z.string().url('Agent URL must be a valid URL').max(2048),
|
||||
apiKey: z.string().optional(),
|
||||
})
|
||||
|
||||
export const a2aSendMessageBodySchema = a2aBaseBodySchema.extend({
|
||||
message: z.string().min(1, 'Message is required'),
|
||||
data: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
|
||||
files: z.array(RawFileInputSchema).max(20).optional(),
|
||||
taskId: z.string().optional(),
|
||||
contextId: z.string().optional(),
|
||||
})
|
||||
|
||||
export const a2aGetTaskBodySchema = a2aBaseBodySchema.extend({
|
||||
taskId: z.string().min(1, 'Task ID is required'),
|
||||
historyLength: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.max(1000, 'History length cannot exceed 1000')
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const a2aCancelTaskBodySchema = a2aBaseBodySchema.extend({
|
||||
taskId: z.string().min(1, 'Task ID is required'),
|
||||
})
|
||||
|
||||
export const a2aGetAgentCardContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/a2a/get-agent-card',
|
||||
body: a2aBaseBodySchema,
|
||||
response: { mode: 'json', schema: genericToolResponseSchema },
|
||||
})
|
||||
|
||||
export const a2aSendMessageContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/a2a/send-message',
|
||||
body: a2aSendMessageBodySchema,
|
||||
response: { mode: 'json', schema: genericToolResponseSchema },
|
||||
})
|
||||
|
||||
export const a2aGetTaskContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/a2a/get-task',
|
||||
body: a2aGetTaskBodySchema,
|
||||
response: { mode: 'json', schema: genericToolResponseSchema },
|
||||
})
|
||||
|
||||
export const a2aCancelTaskContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/a2a/cancel-task',
|
||||
body: a2aCancelTaskBodySchema,
|
||||
response: { mode: 'json', schema: genericToolResponseSchema },
|
||||
})
|
||||
@@ -0,0 +1,396 @@
|
||||
import { z } from 'zod'
|
||||
import type {
|
||||
ContractBody,
|
||||
ContractBodyInput,
|
||||
ContractJsonResponse,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
|
||||
|
||||
const agiloftFileOutputSchema = z.object({
|
||||
name: z.string(),
|
||||
mimeType: z.string(),
|
||||
data: z.string(),
|
||||
size: z.number(),
|
||||
})
|
||||
|
||||
export const agiloftRetrieveResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
output: z.object({
|
||||
file: agiloftFileOutputSchema,
|
||||
}),
|
||||
})
|
||||
|
||||
export const agiloftAttachResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
output: z.object({
|
||||
recordId: z.string(),
|
||||
fieldName: z.string(),
|
||||
fileName: z.string(),
|
||||
totalAttachments: z.number(),
|
||||
}),
|
||||
})
|
||||
|
||||
export const agiloftRetrieveBodySchema = z.object({
|
||||
instanceUrl: z.string().min(1, 'Instance URL is required'),
|
||||
knowledgeBase: z.string().min(1, 'Knowledge base is required'),
|
||||
login: z.string().min(1, 'Login is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
table: z.string().min(1, 'Table is required'),
|
||||
recordId: z.string().min(1, 'Record ID is required'),
|
||||
fieldName: z.string().min(1, 'Field name is required'),
|
||||
position: z.string().min(1, 'Position is required'),
|
||||
})
|
||||
|
||||
export const agiloftAttachBodySchema = z.object({
|
||||
instanceUrl: z.string().min(1, 'Instance URL is required'),
|
||||
knowledgeBase: z.string().min(1, 'Knowledge base is required'),
|
||||
login: z.string().min(1, 'Login is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
table: z.string().min(1, 'Table is required'),
|
||||
recordId: z.string().min(1, 'Record ID is required'),
|
||||
fieldName: z.string().min(1, 'Field name is required'),
|
||||
file: FileInputSchema.optional(),
|
||||
fileName: z.string().optional(),
|
||||
})
|
||||
|
||||
export const agiloftRetrieveContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/agiloft/retrieve',
|
||||
body: agiloftRetrieveBodySchema,
|
||||
response: { mode: 'json', schema: agiloftRetrieveResponseSchema },
|
||||
})
|
||||
|
||||
export const agiloftAttachContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/agiloft/attach',
|
||||
body: agiloftAttachBodySchema,
|
||||
response: { mode: 'json', schema: agiloftAttachResponseSchema },
|
||||
})
|
||||
|
||||
export type AgiloftRetrieveBody = ContractBody<typeof agiloftRetrieveContract>
|
||||
export type AgiloftRetrieveBodyInput = ContractBodyInput<typeof agiloftRetrieveContract>
|
||||
export type AgiloftRetrieveResponse = ContractJsonResponse<typeof agiloftRetrieveContract>
|
||||
export type AgiloftAttachBody = ContractBody<typeof agiloftAttachContract>
|
||||
export type AgiloftAttachBodyInput = ContractBodyInput<typeof agiloftAttachContract>
|
||||
export type AgiloftAttachResponse = ContractJsonResponse<typeof agiloftAttachContract>
|
||||
|
||||
const agiloftBaseFields = {
|
||||
instanceUrl: z.string().min(1, 'Instance URL is required'),
|
||||
knowledgeBase: z.string().min(1, 'Knowledge base is required'),
|
||||
login: z.string().min(1, 'Login is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
table: z.string().min(1, 'Table is required'),
|
||||
} as const
|
||||
|
||||
export const agiloftCreateRecordBodySchema = z.object({
|
||||
...agiloftBaseFields,
|
||||
data: z.string().min(1, 'Data is required'),
|
||||
})
|
||||
|
||||
export const agiloftCreateRecordResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
output: z.object({
|
||||
id: z.string().nullable(),
|
||||
fields: z.record(z.string(), z.unknown()),
|
||||
}),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
|
||||
export const agiloftCreateRecordContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/agiloft/create_record',
|
||||
body: agiloftCreateRecordBodySchema,
|
||||
response: { mode: 'json', schema: agiloftCreateRecordResponseSchema },
|
||||
})
|
||||
|
||||
export type AgiloftCreateRecordBody = ContractBody<typeof agiloftCreateRecordContract>
|
||||
export type AgiloftCreateRecordBodyInput = ContractBodyInput<typeof agiloftCreateRecordContract>
|
||||
export type AgiloftCreateRecordResponse = ContractJsonResponse<typeof agiloftCreateRecordContract>
|
||||
|
||||
export const agiloftReadRecordBodySchema = z.object({
|
||||
...agiloftBaseFields,
|
||||
recordId: z.string().min(1, 'Record ID is required'),
|
||||
fields: z.string().optional(),
|
||||
})
|
||||
|
||||
export const agiloftReadRecordResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
output: z.object({
|
||||
id: z.string().nullable(),
|
||||
fields: z.record(z.string(), z.unknown()),
|
||||
}),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
|
||||
export const agiloftReadRecordContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/agiloft/read_record',
|
||||
body: agiloftReadRecordBodySchema,
|
||||
response: { mode: 'json', schema: agiloftReadRecordResponseSchema },
|
||||
})
|
||||
|
||||
export type AgiloftReadRecordBody = ContractBody<typeof agiloftReadRecordContract>
|
||||
export type AgiloftReadRecordBodyInput = ContractBodyInput<typeof agiloftReadRecordContract>
|
||||
export type AgiloftReadRecordResponse = ContractJsonResponse<typeof agiloftReadRecordContract>
|
||||
|
||||
export const agiloftUpdateRecordBodySchema = z.object({
|
||||
...agiloftBaseFields,
|
||||
recordId: z.string().min(1, 'Record ID is required'),
|
||||
data: z.string().min(1, 'Data is required'),
|
||||
})
|
||||
|
||||
export const agiloftUpdateRecordResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
output: z.object({
|
||||
id: z.string().nullable(),
|
||||
fields: z.record(z.string(), z.unknown()),
|
||||
}),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
|
||||
export const agiloftUpdateRecordContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/agiloft/update_record',
|
||||
body: agiloftUpdateRecordBodySchema,
|
||||
response: { mode: 'json', schema: agiloftUpdateRecordResponseSchema },
|
||||
})
|
||||
|
||||
export type AgiloftUpdateRecordBody = ContractBody<typeof agiloftUpdateRecordContract>
|
||||
export type AgiloftUpdateRecordBodyInput = ContractBodyInput<typeof agiloftUpdateRecordContract>
|
||||
export type AgiloftUpdateRecordResponse = ContractJsonResponse<typeof agiloftUpdateRecordContract>
|
||||
|
||||
export const agiloftDeleteRecordBodySchema = z.object({
|
||||
...agiloftBaseFields,
|
||||
recordId: z.string().min(1, 'Record ID is required'),
|
||||
})
|
||||
|
||||
export const agiloftDeleteRecordResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
output: z.object({
|
||||
id: z.string(),
|
||||
deleted: z.boolean(),
|
||||
}),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
|
||||
export const agiloftDeleteRecordContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/agiloft/delete_record',
|
||||
body: agiloftDeleteRecordBodySchema,
|
||||
response: { mode: 'json', schema: agiloftDeleteRecordResponseSchema },
|
||||
})
|
||||
|
||||
export type AgiloftDeleteRecordBody = ContractBody<typeof agiloftDeleteRecordContract>
|
||||
export type AgiloftDeleteRecordBodyInput = ContractBodyInput<typeof agiloftDeleteRecordContract>
|
||||
export type AgiloftDeleteRecordResponse = ContractJsonResponse<typeof agiloftDeleteRecordContract>
|
||||
|
||||
export const agiloftLockRecordBodySchema = z.object({
|
||||
...agiloftBaseFields,
|
||||
recordId: z.string().min(1, 'Record ID is required'),
|
||||
lockAction: z.enum(['lock', 'unlock', 'check'], {
|
||||
message: 'Lock action must be "lock", "unlock", or "check"',
|
||||
}),
|
||||
})
|
||||
|
||||
export const agiloftLockRecordResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
output: z.object({
|
||||
id: z.string(),
|
||||
lockStatus: z.string(),
|
||||
lockedBy: z.string().nullable(),
|
||||
lockExpiresInMinutes: z.number().nullable(),
|
||||
}),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
|
||||
export const agiloftLockRecordContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/agiloft/lock_record',
|
||||
body: agiloftLockRecordBodySchema,
|
||||
response: { mode: 'json', schema: agiloftLockRecordResponseSchema },
|
||||
})
|
||||
|
||||
export type AgiloftLockRecordBody = ContractBody<typeof agiloftLockRecordContract>
|
||||
export type AgiloftLockRecordBodyInput = ContractBodyInput<typeof agiloftLockRecordContract>
|
||||
export type AgiloftLockRecordResponse = ContractJsonResponse<typeof agiloftLockRecordContract>
|
||||
|
||||
export const agiloftSearchRecordsBodySchema = z.object({
|
||||
...agiloftBaseFields,
|
||||
query: z.string().min(1, 'Query is required'),
|
||||
fields: z.string().optional(),
|
||||
page: z.string().optional(),
|
||||
limit: z.string().optional(),
|
||||
})
|
||||
|
||||
export const agiloftSearchRecordsResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
output: z.object({
|
||||
records: z.array(z.record(z.string(), z.unknown())),
|
||||
totalCount: z.number(),
|
||||
page: z.number(),
|
||||
limit: z.number(),
|
||||
}),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
|
||||
export const agiloftSearchRecordsContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/agiloft/search_records',
|
||||
body: agiloftSearchRecordsBodySchema,
|
||||
response: { mode: 'json', schema: agiloftSearchRecordsResponseSchema },
|
||||
})
|
||||
|
||||
export type AgiloftSearchRecordsBody = ContractBody<typeof agiloftSearchRecordsContract>
|
||||
export type AgiloftSearchRecordsBodyInput = ContractBodyInput<typeof agiloftSearchRecordsContract>
|
||||
export type AgiloftSearchRecordsResponse = ContractJsonResponse<typeof agiloftSearchRecordsContract>
|
||||
|
||||
export const agiloftSelectRecordsBodySchema = z.object({
|
||||
...agiloftBaseFields,
|
||||
where: z.string().min(1, 'Where clause is required'),
|
||||
})
|
||||
|
||||
export const agiloftSelectRecordsResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
output: z.object({
|
||||
recordIds: z.array(z.string()),
|
||||
totalCount: z.number(),
|
||||
}),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
|
||||
export const agiloftSelectRecordsContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/agiloft/select_records',
|
||||
body: agiloftSelectRecordsBodySchema,
|
||||
response: { mode: 'json', schema: agiloftSelectRecordsResponseSchema },
|
||||
})
|
||||
|
||||
export type AgiloftSelectRecordsBody = ContractBody<typeof agiloftSelectRecordsContract>
|
||||
export type AgiloftSelectRecordsBodyInput = ContractBodyInput<typeof agiloftSelectRecordsContract>
|
||||
export type AgiloftSelectRecordsResponse = ContractJsonResponse<typeof agiloftSelectRecordsContract>
|
||||
|
||||
export const agiloftSavedSearchBodySchema = z.object({
|
||||
...agiloftBaseFields,
|
||||
})
|
||||
|
||||
export const agiloftSavedSearchResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
output: z.object({
|
||||
searches: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
label: z.string(),
|
||||
id: z.union([z.string(), z.number()]),
|
||||
description: z.string().nullable(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
|
||||
export const agiloftSavedSearchContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/agiloft/saved_search',
|
||||
body: agiloftSavedSearchBodySchema,
|
||||
response: { mode: 'json', schema: agiloftSavedSearchResponseSchema },
|
||||
})
|
||||
|
||||
export type AgiloftSavedSearchBody = ContractBody<typeof agiloftSavedSearchContract>
|
||||
export type AgiloftSavedSearchBodyInput = ContractBodyInput<typeof agiloftSavedSearchContract>
|
||||
export type AgiloftSavedSearchResponse = ContractJsonResponse<typeof agiloftSavedSearchContract>
|
||||
|
||||
export const agiloftAttachmentInfoBodySchema = z.object({
|
||||
...agiloftBaseFields,
|
||||
recordId: z.string().min(1, 'Record ID is required'),
|
||||
fieldName: z.string().min(1, 'Field name is required'),
|
||||
})
|
||||
|
||||
export const agiloftAttachmentInfoResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
output: z.object({
|
||||
attachments: z.array(
|
||||
z.object({
|
||||
position: z.number(),
|
||||
name: z.string(),
|
||||
size: z.number(),
|
||||
})
|
||||
),
|
||||
totalCount: z.number(),
|
||||
}),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
|
||||
export const agiloftAttachmentInfoContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/agiloft/attachment_info',
|
||||
body: agiloftAttachmentInfoBodySchema,
|
||||
response: { mode: 'json', schema: agiloftAttachmentInfoResponseSchema },
|
||||
})
|
||||
|
||||
export type AgiloftAttachmentInfoBody = ContractBody<typeof agiloftAttachmentInfoContract>
|
||||
export type AgiloftAttachmentInfoBodyInput = ContractBodyInput<typeof agiloftAttachmentInfoContract>
|
||||
export type AgiloftAttachmentInfoResponse = ContractJsonResponse<
|
||||
typeof agiloftAttachmentInfoContract
|
||||
>
|
||||
|
||||
export const agiloftRemoveAttachmentBodySchema = z.object({
|
||||
...agiloftBaseFields,
|
||||
recordId: z.string().min(1, 'Record ID is required'),
|
||||
fieldName: z.string().min(1, 'Field name is required'),
|
||||
position: z.string().min(1, 'Position is required'),
|
||||
})
|
||||
|
||||
export const agiloftRemoveAttachmentResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
output: z.object({
|
||||
recordId: z.string(),
|
||||
fieldName: z.string(),
|
||||
remainingAttachments: z.number(),
|
||||
}),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
|
||||
export const agiloftRemoveAttachmentContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/agiloft/remove_attachment',
|
||||
body: agiloftRemoveAttachmentBodySchema,
|
||||
response: { mode: 'json', schema: agiloftRemoveAttachmentResponseSchema },
|
||||
})
|
||||
|
||||
export type AgiloftRemoveAttachmentBody = ContractBody<typeof agiloftRemoveAttachmentContract>
|
||||
export type AgiloftRemoveAttachmentBodyInput = ContractBodyInput<
|
||||
typeof agiloftRemoveAttachmentContract
|
||||
>
|
||||
export type AgiloftRemoveAttachmentResponse = ContractJsonResponse<
|
||||
typeof agiloftRemoveAttachmentContract
|
||||
>
|
||||
|
||||
export const agiloftGetChoiceLineIdBodySchema = z.object({
|
||||
...agiloftBaseFields,
|
||||
fieldName: z.string().min(1, 'Field name is required'),
|
||||
value: z.string().min(1, 'Value is required'),
|
||||
})
|
||||
|
||||
export const agiloftGetChoiceLineIdResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
output: z.object({
|
||||
choiceLineId: z.number().nullable(),
|
||||
}),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
|
||||
export const agiloftGetChoiceLineIdContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/agiloft/get_choice_line_id',
|
||||
body: agiloftGetChoiceLineIdBodySchema,
|
||||
response: { mode: 'json', schema: agiloftGetChoiceLineIdResponseSchema },
|
||||
})
|
||||
|
||||
export type AgiloftGetChoiceLineIdBody = ContractBody<typeof agiloftGetChoiceLineIdContract>
|
||||
export type AgiloftGetChoiceLineIdBodyInput = ContractBodyInput<
|
||||
typeof agiloftGetChoiceLineIdContract
|
||||
>
|
||||
export type AgiloftGetChoiceLineIdResponse = ContractJsonResponse<
|
||||
typeof agiloftGetChoiceLineIdContract
|
||||
>
|
||||
@@ -0,0 +1,376 @@
|
||||
import { z } from 'zod'
|
||||
import type {
|
||||
ContractBody,
|
||||
ContractBodyInput,
|
||||
ContractJsonResponse,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const asanaUserSummarySchema = z.object({
|
||||
gid: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
|
||||
const asanaCreatedBySchema = asanaUserSummarySchema.extend({
|
||||
resource_type: z.string().optional(),
|
||||
})
|
||||
|
||||
const asanaTaskSchema = z.object({
|
||||
gid: z.string(),
|
||||
resource_type: z.string().optional(),
|
||||
resource_subtype: z.string().optional(),
|
||||
name: z.string(),
|
||||
notes: z.string(),
|
||||
completed: z.boolean(),
|
||||
assignee: asanaUserSummarySchema.optional(),
|
||||
created_by: asanaCreatedBySchema.optional(),
|
||||
due_on: z.string().optional(),
|
||||
created_at: z.string().optional(),
|
||||
modified_at: z.string().optional(),
|
||||
})
|
||||
|
||||
const asanaTaskMutationResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
ts: z.string(),
|
||||
gid: z.string(),
|
||||
name: z.string(),
|
||||
notes: z.string(),
|
||||
completed: z.boolean(),
|
||||
created_at: z.string().optional(),
|
||||
modified_at: z.string().optional(),
|
||||
permalink_url: z.string().optional(),
|
||||
})
|
||||
|
||||
const asanaTasksResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
ts: z.string(),
|
||||
tasks: z.array(asanaTaskSchema),
|
||||
next_page: z.unknown().optional(),
|
||||
})
|
||||
|
||||
const asanaSingleTaskResponseSchema = asanaTaskSchema.extend({
|
||||
success: z.literal(true),
|
||||
ts: z.string(),
|
||||
})
|
||||
|
||||
const asanaProjectSchema = z.object({
|
||||
gid: z.string(),
|
||||
name: z.string(),
|
||||
resource_type: z.string(),
|
||||
})
|
||||
|
||||
const asanaProjectsResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
ts: z.string(),
|
||||
projects: z.array(asanaProjectSchema),
|
||||
})
|
||||
|
||||
const asanaCommentResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
ts: z.string(),
|
||||
gid: z.string(),
|
||||
text: z.string(),
|
||||
created_at: z.string().optional(),
|
||||
created_by: asanaUserSummarySchema.optional(),
|
||||
})
|
||||
|
||||
const asanaGetTaskResponseSchema = z.union([
|
||||
asanaSingleTaskResponseSchema,
|
||||
asanaTasksResponseSchema,
|
||||
])
|
||||
|
||||
const asanaProjectRecordResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
ts: z.string(),
|
||||
gid: z.string(),
|
||||
name: z.string(),
|
||||
notes: z.string(),
|
||||
archived: z.boolean().optional(),
|
||||
color: z.string().nullable().optional(),
|
||||
created_at: z.string().optional(),
|
||||
modified_at: z.string().optional(),
|
||||
permalink_url: z.string().optional(),
|
||||
})
|
||||
|
||||
const asanaWorkspaceSchema = z.object({
|
||||
gid: z.string(),
|
||||
name: z.string(),
|
||||
resource_type: z.string().optional(),
|
||||
})
|
||||
|
||||
const asanaListWorkspacesResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
ts: z.string(),
|
||||
workspaces: z.array(asanaWorkspaceSchema),
|
||||
})
|
||||
|
||||
const asanaDeleteTaskResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
ts: z.string(),
|
||||
gid: z.string(),
|
||||
deleted: z.literal(true),
|
||||
})
|
||||
|
||||
const asanaAddFollowersResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
ts: z.string(),
|
||||
gid: z.string(),
|
||||
name: z.string(),
|
||||
followers: z.array(asanaUserSummarySchema),
|
||||
})
|
||||
|
||||
const asanaSectionSchema = z.object({
|
||||
gid: z.string(),
|
||||
name: z.string(),
|
||||
resource_type: z.string().optional(),
|
||||
})
|
||||
|
||||
const asanaSectionResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
ts: z.string(),
|
||||
gid: z.string(),
|
||||
name: z.string(),
|
||||
created_at: z.string().optional(),
|
||||
})
|
||||
|
||||
const asanaListSectionsResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
ts: z.string(),
|
||||
sections: z.array(asanaSectionSchema),
|
||||
})
|
||||
|
||||
export const asanaAddCommentBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
taskGid: z.string().min(1, 'Task GID is required'),
|
||||
text: z.string().min(1, 'Comment text is required'),
|
||||
})
|
||||
|
||||
export const asanaCreateTaskBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
name: z.string().min(1, 'Task name is required'),
|
||||
workspace: z.string().min(1, 'Workspace GID is required'),
|
||||
notes: z.string().nullish(),
|
||||
assignee: z.string().nullish(),
|
||||
due_on: z.string().nullish(),
|
||||
})
|
||||
|
||||
export const asanaGetProjectsBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
workspace: z.string().min(1, 'Workspace is required'),
|
||||
})
|
||||
|
||||
export const asanaGetTaskBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
taskGid: z.string().nullish(),
|
||||
workspace: z.string().nullish(),
|
||||
project: z.string().nullish(),
|
||||
limit: z.union([z.string(), z.number()]).nullish(),
|
||||
})
|
||||
|
||||
export const asanaSearchTasksBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
workspace: z.string().min(1, 'Workspace is required'),
|
||||
text: z.string().nullish(),
|
||||
assignee: z.string().nullish(),
|
||||
projects: z.array(z.string()).nullish(),
|
||||
completed: z.boolean().nullish(),
|
||||
})
|
||||
|
||||
export const asanaUpdateTaskBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
taskGid: z.string().min(1, 'Task GID is required'),
|
||||
name: z.string().nullish(),
|
||||
notes: z.string().nullish(),
|
||||
assignee: z.string().nullish(),
|
||||
completed: z.boolean().nullish(),
|
||||
due_on: z.string().nullish(),
|
||||
})
|
||||
|
||||
export const asanaCreateProjectBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
workspace: z.string().min(1, 'Workspace GID is required'),
|
||||
name: z.string().min(1, 'Project name is required'),
|
||||
notes: z.string().nullish(),
|
||||
})
|
||||
|
||||
export const asanaGetProjectBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
projectGid: z.string().min(1, 'Project GID is required'),
|
||||
})
|
||||
|
||||
export const asanaListWorkspacesBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
})
|
||||
|
||||
export const asanaCreateSubtaskBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
taskGid: z.string().min(1, 'Parent task GID is required'),
|
||||
name: z.string().min(1, 'Subtask name is required'),
|
||||
notes: z.string().nullish(),
|
||||
assignee: z.string().nullish(),
|
||||
due_on: z.string().nullish(),
|
||||
})
|
||||
|
||||
export const asanaDeleteTaskBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
taskGid: z.string().min(1, 'Task GID is required'),
|
||||
})
|
||||
|
||||
export const asanaAddFollowersBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
taskGid: z.string().min(1, 'Task GID is required'),
|
||||
followers: z.array(z.string().min(1)).min(1, 'At least one follower GID is required'),
|
||||
})
|
||||
|
||||
export const asanaCreateSectionBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
projectGid: z.string().min(1, 'Project GID is required'),
|
||||
name: z.string().min(1, 'Section name is required'),
|
||||
})
|
||||
|
||||
export const asanaListSectionsBodySchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
projectGid: z.string().min(1, 'Project GID is required'),
|
||||
})
|
||||
|
||||
export const asanaAddCommentContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/asana/add-comment',
|
||||
body: asanaAddCommentBodySchema,
|
||||
response: { mode: 'json', schema: asanaCommentResponseSchema },
|
||||
})
|
||||
|
||||
export const asanaCreateTaskContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/asana/create-task',
|
||||
body: asanaCreateTaskBodySchema,
|
||||
response: { mode: 'json', schema: asanaTaskMutationResponseSchema },
|
||||
})
|
||||
|
||||
export const asanaGetProjectsContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/asana/get-projects',
|
||||
body: asanaGetProjectsBodySchema,
|
||||
response: { mode: 'json', schema: asanaProjectsResponseSchema },
|
||||
})
|
||||
|
||||
export const asanaGetTaskContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/asana/get-task',
|
||||
body: asanaGetTaskBodySchema,
|
||||
response: { mode: 'json', schema: asanaGetTaskResponseSchema },
|
||||
})
|
||||
|
||||
export const asanaSearchTasksContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/asana/search-tasks',
|
||||
body: asanaSearchTasksBodySchema,
|
||||
response: { mode: 'json', schema: asanaTasksResponseSchema },
|
||||
})
|
||||
|
||||
export const asanaUpdateTaskContract = defineRouteContract({
|
||||
method: 'PUT',
|
||||
path: '/api/tools/asana/update-task',
|
||||
body: asanaUpdateTaskBodySchema,
|
||||
response: { mode: 'json', schema: asanaTaskMutationResponseSchema },
|
||||
})
|
||||
|
||||
export const asanaCreateProjectContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/asana/create-project',
|
||||
body: asanaCreateProjectBodySchema,
|
||||
response: { mode: 'json', schema: asanaProjectRecordResponseSchema },
|
||||
})
|
||||
|
||||
export const asanaGetProjectContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/asana/get-project',
|
||||
body: asanaGetProjectBodySchema,
|
||||
response: { mode: 'json', schema: asanaProjectRecordResponseSchema },
|
||||
})
|
||||
|
||||
export const asanaListWorkspacesContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/asana/list-workspaces',
|
||||
body: asanaListWorkspacesBodySchema,
|
||||
response: { mode: 'json', schema: asanaListWorkspacesResponseSchema },
|
||||
})
|
||||
|
||||
export const asanaCreateSubtaskContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/asana/create-subtask',
|
||||
body: asanaCreateSubtaskBodySchema,
|
||||
response: { mode: 'json', schema: asanaTaskMutationResponseSchema },
|
||||
})
|
||||
|
||||
export const asanaDeleteTaskContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/asana/delete-task',
|
||||
body: asanaDeleteTaskBodySchema,
|
||||
response: { mode: 'json', schema: asanaDeleteTaskResponseSchema },
|
||||
})
|
||||
|
||||
export const asanaAddFollowersContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/asana/add-followers',
|
||||
body: asanaAddFollowersBodySchema,
|
||||
response: { mode: 'json', schema: asanaAddFollowersResponseSchema },
|
||||
})
|
||||
|
||||
export const asanaCreateSectionContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/asana/create-section',
|
||||
body: asanaCreateSectionBodySchema,
|
||||
response: { mode: 'json', schema: asanaSectionResponseSchema },
|
||||
})
|
||||
|
||||
export const asanaListSectionsContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/asana/list-sections',
|
||||
body: asanaListSectionsBodySchema,
|
||||
response: { mode: 'json', schema: asanaListSectionsResponseSchema },
|
||||
})
|
||||
|
||||
export type AsanaCreateProjectBody = ContractBody<typeof asanaCreateProjectContract>
|
||||
export type AsanaCreateProjectBodyInput = ContractBodyInput<typeof asanaCreateProjectContract>
|
||||
export type AsanaCreateProjectResponse = ContractJsonResponse<typeof asanaCreateProjectContract>
|
||||
export type AsanaGetProjectBody = ContractBody<typeof asanaGetProjectContract>
|
||||
export type AsanaGetProjectBodyInput = ContractBodyInput<typeof asanaGetProjectContract>
|
||||
export type AsanaGetProjectResponse = ContractJsonResponse<typeof asanaGetProjectContract>
|
||||
export type AsanaListWorkspacesBody = ContractBody<typeof asanaListWorkspacesContract>
|
||||
export type AsanaListWorkspacesBodyInput = ContractBodyInput<typeof asanaListWorkspacesContract>
|
||||
export type AsanaListWorkspacesResponse = ContractJsonResponse<typeof asanaListWorkspacesContract>
|
||||
export type AsanaCreateSubtaskBody = ContractBody<typeof asanaCreateSubtaskContract>
|
||||
export type AsanaCreateSubtaskBodyInput = ContractBodyInput<typeof asanaCreateSubtaskContract>
|
||||
export type AsanaCreateSubtaskResponse = ContractJsonResponse<typeof asanaCreateSubtaskContract>
|
||||
export type AsanaDeleteTaskBody = ContractBody<typeof asanaDeleteTaskContract>
|
||||
export type AsanaDeleteTaskBodyInput = ContractBodyInput<typeof asanaDeleteTaskContract>
|
||||
export type AsanaDeleteTaskResponse = ContractJsonResponse<typeof asanaDeleteTaskContract>
|
||||
export type AsanaAddFollowersBody = ContractBody<typeof asanaAddFollowersContract>
|
||||
export type AsanaAddFollowersBodyInput = ContractBodyInput<typeof asanaAddFollowersContract>
|
||||
export type AsanaAddFollowersResponse = ContractJsonResponse<typeof asanaAddFollowersContract>
|
||||
export type AsanaCreateSectionBody = ContractBody<typeof asanaCreateSectionContract>
|
||||
export type AsanaCreateSectionBodyInput = ContractBodyInput<typeof asanaCreateSectionContract>
|
||||
export type AsanaCreateSectionResponse = ContractJsonResponse<typeof asanaCreateSectionContract>
|
||||
export type AsanaListSectionsBody = ContractBody<typeof asanaListSectionsContract>
|
||||
export type AsanaListSectionsBodyInput = ContractBodyInput<typeof asanaListSectionsContract>
|
||||
export type AsanaListSectionsResponse = ContractJsonResponse<typeof asanaListSectionsContract>
|
||||
|
||||
export type AsanaAddCommentBody = ContractBody<typeof asanaAddCommentContract>
|
||||
export type AsanaAddCommentBodyInput = ContractBodyInput<typeof asanaAddCommentContract>
|
||||
export type AsanaAddCommentResponse = ContractJsonResponse<typeof asanaAddCommentContract>
|
||||
export type AsanaCreateTaskBody = ContractBody<typeof asanaCreateTaskContract>
|
||||
export type AsanaCreateTaskBodyInput = ContractBodyInput<typeof asanaCreateTaskContract>
|
||||
export type AsanaCreateTaskResponse = ContractJsonResponse<typeof asanaCreateTaskContract>
|
||||
export type AsanaGetProjectsBody = ContractBody<typeof asanaGetProjectsContract>
|
||||
export type AsanaGetProjectsBodyInput = ContractBodyInput<typeof asanaGetProjectsContract>
|
||||
export type AsanaGetProjectsResponse = ContractJsonResponse<typeof asanaGetProjectsContract>
|
||||
export type AsanaGetTaskBody = ContractBody<typeof asanaGetTaskContract>
|
||||
export type AsanaGetTaskBodyInput = ContractBodyInput<typeof asanaGetTaskContract>
|
||||
export type AsanaGetTaskResponse = ContractJsonResponse<typeof asanaGetTaskContract>
|
||||
export type AsanaSearchTasksBody = ContractBody<typeof asanaSearchTasksContract>
|
||||
export type AsanaSearchTasksBodyInput = ContractBodyInput<typeof asanaSearchTasksContract>
|
||||
export type AsanaSearchTasksResponse = ContractJsonResponse<typeof asanaSearchTasksContract>
|
||||
export type AsanaUpdateTaskBody = ContractBody<typeof asanaUpdateTaskContract>
|
||||
export type AsanaUpdateTaskBodyInput = ContractBodyInput<typeof asanaUpdateTaskContract>
|
||||
export type AsanaUpdateTaskResponse = ContractJsonResponse<typeof asanaUpdateTaskContract>
|
||||
@@ -0,0 +1,38 @@
|
||||
import { z } from 'zod'
|
||||
import type {
|
||||
ContractBody,
|
||||
ContractBodyInput,
|
||||
ContractJsonResponse,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const CreateApplicationSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
name: z.string().min(1, 'Application name is required').max(64),
|
||||
description: z.string().max(1024).nullish(),
|
||||
})
|
||||
|
||||
const CreateApplicationResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const awsAppConfigCreateApplicationContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/appconfig/create-application',
|
||||
body: CreateApplicationSchema,
|
||||
response: { mode: 'json', schema: CreateApplicationResponseSchema },
|
||||
})
|
||||
export type AwsAppConfigCreateApplicationRequest = ContractBodyInput<
|
||||
typeof awsAppConfigCreateApplicationContract
|
||||
>
|
||||
export type AwsAppConfigCreateApplicationBody = ContractBody<
|
||||
typeof awsAppConfigCreateApplicationContract
|
||||
>
|
||||
export type AwsAppConfigCreateApplicationResponse = ContractJsonResponse<
|
||||
typeof awsAppConfigCreateApplicationContract
|
||||
>
|
||||
@@ -0,0 +1,44 @@
|
||||
import { z } from 'zod'
|
||||
import type {
|
||||
ContractBody,
|
||||
ContractBodyInput,
|
||||
ContractJsonResponse,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const CreateConfigurationProfileSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
applicationId: z.string().min(1, 'Application ID is required'),
|
||||
name: z.string().min(1, 'Configuration profile name is required').max(128),
|
||||
locationUri: z.string().min(1, 'Location URI is required').max(2048),
|
||||
description: z.string().max(1024).nullish(),
|
||||
retrievalRoleArn: z.string().nullish(),
|
||||
type: z.string().nullish(),
|
||||
})
|
||||
|
||||
const CreateConfigurationProfileResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
applicationId: z.string(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
locationUri: z.string().nullable(),
|
||||
type: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const awsAppConfigCreateConfigurationProfileContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/appconfig/create-configuration-profile',
|
||||
body: CreateConfigurationProfileSchema,
|
||||
response: { mode: 'json', schema: CreateConfigurationProfileResponseSchema },
|
||||
})
|
||||
export type AwsAppConfigCreateConfigurationProfileRequest = ContractBodyInput<
|
||||
typeof awsAppConfigCreateConfigurationProfileContract
|
||||
>
|
||||
export type AwsAppConfigCreateConfigurationProfileBody = ContractBody<
|
||||
typeof awsAppConfigCreateConfigurationProfileContract
|
||||
>
|
||||
export type AwsAppConfigCreateConfigurationProfileResponse = ContractJsonResponse<
|
||||
typeof awsAppConfigCreateConfigurationProfileContract
|
||||
>
|
||||
@@ -0,0 +1,40 @@
|
||||
import { z } from 'zod'
|
||||
import type {
|
||||
ContractBody,
|
||||
ContractBodyInput,
|
||||
ContractJsonResponse,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const CreateEnvironmentSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
applicationId: z.string().min(1, 'Application ID is required'),
|
||||
name: z.string().min(1, 'Environment name is required').max(64),
|
||||
description: z.string().max(1024).nullish(),
|
||||
})
|
||||
|
||||
const CreateEnvironmentResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
applicationId: z.string(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
state: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const awsAppConfigCreateEnvironmentContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/appconfig/create-environment',
|
||||
body: CreateEnvironmentSchema,
|
||||
response: { mode: 'json', schema: CreateEnvironmentResponseSchema },
|
||||
})
|
||||
export type AwsAppConfigCreateEnvironmentRequest = ContractBodyInput<
|
||||
typeof awsAppConfigCreateEnvironmentContract
|
||||
>
|
||||
export type AwsAppConfigCreateEnvironmentBody = ContractBody<
|
||||
typeof awsAppConfigCreateEnvironmentContract
|
||||
>
|
||||
export type AwsAppConfigCreateEnvironmentResponse = ContractJsonResponse<
|
||||
typeof awsAppConfigCreateEnvironmentContract
|
||||
>
|
||||
@@ -0,0 +1,45 @@
|
||||
import { z } from 'zod'
|
||||
import type {
|
||||
ContractBody,
|
||||
ContractBodyInput,
|
||||
ContractJsonResponse,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const CreateHostedConfigurationVersionSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
applicationId: z.string().min(1, 'Application ID is required'),
|
||||
configurationProfileId: z.string().min(1, 'Configuration profile ID is required'),
|
||||
content: z.string().min(1, 'Content is required'),
|
||||
contentType: z.string().min(1, 'Content type is required').max(255),
|
||||
description: z.string().max(1024).nullish(),
|
||||
latestVersionNumber: z.number().int().min(0).nullish(),
|
||||
versionLabel: z.string().max(64).nullish(),
|
||||
})
|
||||
|
||||
const CreateHostedConfigurationVersionResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
applicationId: z.string(),
|
||||
configurationProfileId: z.string(),
|
||||
versionNumber: z.number().nullable(),
|
||||
contentType: z.string().nullable(),
|
||||
versionLabel: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const awsAppConfigCreateHostedConfigurationVersionContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/appconfig/create-hosted-configuration-version',
|
||||
body: CreateHostedConfigurationVersionSchema,
|
||||
response: { mode: 'json', schema: CreateHostedConfigurationVersionResponseSchema },
|
||||
})
|
||||
export type AwsAppConfigCreateHostedConfigurationVersionRequest = ContractBodyInput<
|
||||
typeof awsAppConfigCreateHostedConfigurationVersionContract
|
||||
>
|
||||
export type AwsAppConfigCreateHostedConfigurationVersionBody = ContractBody<
|
||||
typeof awsAppConfigCreateHostedConfigurationVersionContract
|
||||
>
|
||||
export type AwsAppConfigCreateHostedConfigurationVersionResponse = ContractJsonResponse<
|
||||
typeof awsAppConfigCreateHostedConfigurationVersionContract
|
||||
>
|
||||
@@ -0,0 +1,35 @@
|
||||
import { z } from 'zod'
|
||||
import type {
|
||||
ContractBody,
|
||||
ContractBodyInput,
|
||||
ContractJsonResponse,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const DeleteApplicationSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
applicationId: z.string().min(1, 'Application ID is required'),
|
||||
})
|
||||
|
||||
const DeleteApplicationResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
export const awsAppConfigDeleteApplicationContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/appconfig/delete-application',
|
||||
body: DeleteApplicationSchema,
|
||||
response: { mode: 'json', schema: DeleteApplicationResponseSchema },
|
||||
})
|
||||
export type AwsAppConfigDeleteApplicationRequest = ContractBodyInput<
|
||||
typeof awsAppConfigDeleteApplicationContract
|
||||
>
|
||||
export type AwsAppConfigDeleteApplicationBody = ContractBody<
|
||||
typeof awsAppConfigDeleteApplicationContract
|
||||
>
|
||||
export type AwsAppConfigDeleteApplicationResponse = ContractJsonResponse<
|
||||
typeof awsAppConfigDeleteApplicationContract
|
||||
>
|
||||
@@ -0,0 +1,37 @@
|
||||
import { z } from 'zod'
|
||||
import type {
|
||||
ContractBody,
|
||||
ContractBodyInput,
|
||||
ContractJsonResponse,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const DeleteConfigurationProfileSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
applicationId: z.string().min(1, 'Application ID is required'),
|
||||
configurationProfileId: z.string().min(1, 'Configuration profile ID is required'),
|
||||
})
|
||||
|
||||
const DeleteConfigurationProfileResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
applicationId: z.string(),
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
export const awsAppConfigDeleteConfigurationProfileContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/appconfig/delete-configuration-profile',
|
||||
body: DeleteConfigurationProfileSchema,
|
||||
response: { mode: 'json', schema: DeleteConfigurationProfileResponseSchema },
|
||||
})
|
||||
export type AwsAppConfigDeleteConfigurationProfileRequest = ContractBodyInput<
|
||||
typeof awsAppConfigDeleteConfigurationProfileContract
|
||||
>
|
||||
export type AwsAppConfigDeleteConfigurationProfileBody = ContractBody<
|
||||
typeof awsAppConfigDeleteConfigurationProfileContract
|
||||
>
|
||||
export type AwsAppConfigDeleteConfigurationProfileResponse = ContractJsonResponse<
|
||||
typeof awsAppConfigDeleteConfigurationProfileContract
|
||||
>
|
||||
@@ -0,0 +1,37 @@
|
||||
import { z } from 'zod'
|
||||
import type {
|
||||
ContractBody,
|
||||
ContractBodyInput,
|
||||
ContractJsonResponse,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const DeleteEnvironmentSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
applicationId: z.string().min(1, 'Application ID is required'),
|
||||
environmentId: z.string().min(1, 'Environment ID is required'),
|
||||
})
|
||||
|
||||
const DeleteEnvironmentResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
applicationId: z.string(),
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
export const awsAppConfigDeleteEnvironmentContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/appconfig/delete-environment',
|
||||
body: DeleteEnvironmentSchema,
|
||||
response: { mode: 'json', schema: DeleteEnvironmentResponseSchema },
|
||||
})
|
||||
export type AwsAppConfigDeleteEnvironmentRequest = ContractBodyInput<
|
||||
typeof awsAppConfigDeleteEnvironmentContract
|
||||
>
|
||||
export type AwsAppConfigDeleteEnvironmentBody = ContractBody<
|
||||
typeof awsAppConfigDeleteEnvironmentContract
|
||||
>
|
||||
export type AwsAppConfigDeleteEnvironmentResponse = ContractJsonResponse<
|
||||
typeof awsAppConfigDeleteEnvironmentContract
|
||||
>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { z } from 'zod'
|
||||
import type {
|
||||
ContractBody,
|
||||
ContractBodyInput,
|
||||
ContractJsonResponse,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const DeleteHostedConfigurationVersionSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
applicationId: z.string().min(1, 'Application ID is required'),
|
||||
configurationProfileId: z.string().min(1, 'Configuration profile ID is required'),
|
||||
versionNumber: z.number().int().min(1, 'Version number is required'),
|
||||
})
|
||||
|
||||
const DeleteHostedConfigurationVersionResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
applicationId: z.string(),
|
||||
configurationProfileId: z.string(),
|
||||
versionNumber: z.number(),
|
||||
})
|
||||
|
||||
export const awsAppConfigDeleteHostedConfigurationVersionContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/appconfig/delete-hosted-configuration-version',
|
||||
body: DeleteHostedConfigurationVersionSchema,
|
||||
response: { mode: 'json', schema: DeleteHostedConfigurationVersionResponseSchema },
|
||||
})
|
||||
export type AwsAppConfigDeleteHostedConfigurationVersionRequest = ContractBodyInput<
|
||||
typeof awsAppConfigDeleteHostedConfigurationVersionContract
|
||||
>
|
||||
export type AwsAppConfigDeleteHostedConfigurationVersionBody = ContractBody<
|
||||
typeof awsAppConfigDeleteHostedConfigurationVersionContract
|
||||
>
|
||||
export type AwsAppConfigDeleteHostedConfigurationVersionResponse = ContractJsonResponse<
|
||||
typeof awsAppConfigDeleteHostedConfigurationVersionContract
|
||||
>
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from 'zod'
|
||||
import type {
|
||||
ContractBody,
|
||||
ContractBodyInput,
|
||||
ContractJsonResponse,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const GetApplicationSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
applicationId: z.string().min(1, 'Application ID is required'),
|
||||
})
|
||||
|
||||
const GetApplicationResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const awsAppConfigGetApplicationContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/appconfig/get-application',
|
||||
body: GetApplicationSchema,
|
||||
response: { mode: 'json', schema: GetApplicationResponseSchema },
|
||||
})
|
||||
export type AwsAppConfigGetApplicationRequest = ContractBodyInput<
|
||||
typeof awsAppConfigGetApplicationContract
|
||||
>
|
||||
export type AwsAppConfigGetApplicationBody = ContractBody<typeof awsAppConfigGetApplicationContract>
|
||||
export type AwsAppConfigGetApplicationResponse = ContractJsonResponse<
|
||||
typeof awsAppConfigGetApplicationContract
|
||||
>
|
||||
@@ -0,0 +1,42 @@
|
||||
import { z } from 'zod'
|
||||
import type {
|
||||
ContractBody,
|
||||
ContractBodyInput,
|
||||
ContractJsonResponse,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const GetConfigurationProfileSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
applicationId: z.string().min(1, 'Application ID is required'),
|
||||
configurationProfileId: z.string().min(1, 'Configuration profile ID is required'),
|
||||
})
|
||||
|
||||
const GetConfigurationProfileResponseSchema = z.object({
|
||||
applicationId: z.string(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
locationUri: z.string().nullable(),
|
||||
retrievalRoleArn: z.string().nullable(),
|
||||
type: z.string().nullable(),
|
||||
validators: z.array(z.object({ type: z.string() })),
|
||||
})
|
||||
|
||||
export const awsAppConfigGetConfigurationProfileContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/appconfig/get-configuration-profile',
|
||||
body: GetConfigurationProfileSchema,
|
||||
response: { mode: 'json', schema: GetConfigurationProfileResponseSchema },
|
||||
})
|
||||
export type AwsAppConfigGetConfigurationProfileRequest = ContractBodyInput<
|
||||
typeof awsAppConfigGetConfigurationProfileContract
|
||||
>
|
||||
export type AwsAppConfigGetConfigurationProfileBody = ContractBody<
|
||||
typeof awsAppConfigGetConfigurationProfileContract
|
||||
>
|
||||
export type AwsAppConfigGetConfigurationProfileResponse = ContractJsonResponse<
|
||||
typeof awsAppConfigGetConfigurationProfileContract
|
||||
>
|
||||
@@ -0,0 +1,38 @@
|
||||
import { z } from 'zod'
|
||||
import type {
|
||||
ContractBody,
|
||||
ContractBodyInput,
|
||||
ContractJsonResponse,
|
||||
} from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
|
||||
const GetConfigurationSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
applicationId: z.string().min(1, 'Application ID or name is required'),
|
||||
environmentId: z.string().min(1, 'Environment ID or name is required'),
|
||||
configurationProfileId: z.string().min(1, 'Configuration profile ID or name is required'),
|
||||
})
|
||||
|
||||
const GetConfigurationResponseSchema = z.object({
|
||||
configuration: z.string(),
|
||||
contentType: z.string().nullable(),
|
||||
versionLabel: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const awsAppConfigGetConfigurationContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/appconfig/get-configuration',
|
||||
body: GetConfigurationSchema,
|
||||
response: { mode: 'json', schema: GetConfigurationResponseSchema },
|
||||
})
|
||||
export type AwsAppConfigGetConfigurationRequest = ContractBodyInput<
|
||||
typeof awsAppConfigGetConfigurationContract
|
||||
>
|
||||
export type AwsAppConfigGetConfigurationBody = ContractBody<
|
||||
typeof awsAppConfigGetConfigurationContract
|
||||
>
|
||||
export type AwsAppConfigGetConfigurationResponse = ContractJsonResponse<
|
||||
typeof awsAppConfigGetConfigurationContract
|
||||
>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user