chore: import upstream snapshot with attribution
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) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,88 @@
import { z } from 'zod'
import {
type ContractJsonResponse,
type ContractQuery,
type ContractQueryInput,
defineRouteContract,
} from '@/lib/api/contracts/types'
import {
adminV1PaginationMetaSchema,
adminV1QueryStringSchema,
adminV1SingleResponseSchema,
} from '@/lib/api/contracts/v1/admin/shared'
export const adminV1PermissionGroupSchema = z.object({
id: z.string(),
organizationId: z.string(),
organizationName: z.string().nullable(),
name: z.string(),
description: z.string().nullable(),
isDefault: z.boolean(),
memberCount: z.number(),
createdAt: z.string(),
createdByUserId: z.string(),
createdByEmail: z.string().nullable(),
})
export const adminV1AccessControlQuerySchema = z.object({
organizationId: adminV1QueryStringSchema,
})
export const adminV1AccessControlDeleteQuerySchema = adminV1AccessControlQuerySchema
.extend({
reason: adminV1QueryStringSchema.default('Enterprise plan churn cleanup'),
})
.refine((query) => Boolean(query.organizationId), {
error: 'organizationId is required',
})
const adminV1AccessControlListResultSchema = z.object({
data: z.array(adminV1PermissionGroupSchema),
pagination: adminV1PaginationMetaSchema,
})
const adminV1AccessControlDeleteResultSchema = z.object({
success: z.literal(true),
deletedCount: z.number(),
membersRemoved: z.number(),
reason: z.string().optional(),
message: z.string().optional(),
})
export const adminV1ListAccessControlContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/access-control',
query: adminV1AccessControlQuerySchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1AccessControlListResultSchema),
},
})
export const adminV1DeleteAccessControlContract = defineRouteContract({
method: 'DELETE',
path: '/api/v1/admin/access-control',
query: adminV1AccessControlDeleteQuerySchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1AccessControlDeleteResultSchema),
},
})
export type AdminV1PermissionGroup = z.output<typeof adminV1PermissionGroupSchema>
export type AdminV1ListAccessControlQueryInput = ContractQueryInput<
typeof adminV1ListAccessControlContract
>
export type AdminV1ListAccessControlQuery = ContractQuery<typeof adminV1ListAccessControlContract>
export type AdminV1DeleteAccessControlQueryInput = ContractQueryInput<
typeof adminV1DeleteAccessControlContract
>
export type AdminV1DeleteAccessControlQuery = ContractQuery<
typeof adminV1DeleteAccessControlContract
>
export type AdminV1ListAccessControlResponse = ContractJsonResponse<
typeof adminV1ListAccessControlContract
>
export type AdminV1DeleteAccessControlResponse = ContractJsonResponse<
typeof adminV1DeleteAccessControlContract
>
@@ -0,0 +1,111 @@
import { z } from 'zod'
import { type ContractJsonResponse, defineRouteContract } from '@/lib/api/contracts/types'
import {
adminV1IdParamsSchema,
adminV1ListResponseSchema,
adminV1PaginationQuerySchema,
adminV1QueryStringSchema,
adminV1SingleResponseSchema,
adminV1SubscriptionSchema,
lastQueryValue,
} from '@/lib/api/contracts/v1/admin/shared'
export const adminV1ListSubscriptionsQuerySchema = adminV1PaginationQuerySchema.extend({
plan: adminV1QueryStringSchema,
status: adminV1QueryStringSchema,
})
export const adminV1CancelSubscriptionQuerySchema = z.object({
atPeriodEnd: z
.preprocess(lastQueryValue, z.unknown())
.pipe(z.enum(['true', 'false']).catch('false'))
.transform((value) => value === 'true'),
reason: adminV1QueryStringSchema.default('Admin cancellation (no reason provided)'),
})
const adminV1CancelSubscriptionResultSchema = z.object({
success: z.literal(true),
message: z.string(),
subscriptionId: z.string(),
stripeSubscriptionId: z.string(),
atPeriodEnd: z.boolean(),
periodEnd: z.string().nullable().optional(),
})
export const adminV1ListSubscriptionsContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/subscriptions',
query: adminV1ListSubscriptionsQuerySchema,
response: {
mode: 'json',
schema: adminV1ListResponseSchema(adminV1SubscriptionSchema),
},
})
export const adminV1GetSubscriptionContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/subscriptions/[id]',
params: adminV1IdParamsSchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1SubscriptionSchema),
},
})
export const adminV1CancelSubscriptionContract = defineRouteContract({
method: 'DELETE',
path: '/api/v1/admin/subscriptions/[id]',
params: adminV1IdParamsSchema,
query: adminV1CancelSubscriptionQuerySchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1CancelSubscriptionResultSchema),
},
})
export const adminV1IssueCreditsBodySchema = z
.object({
userId: z.string({ error: 'userId must be a string' }).optional(),
email: z.string({ error: 'email must be a string' }).optional(),
amount: z
.number({ error: 'amount must be a positive number' })
.finite({ error: 'amount must be a positive number' })
.positive({ error: 'amount must be a positive number' }),
reason: z.string().optional(),
})
.refine((body) => body.userId || body.email, {
error: 'Either userId or email is required',
})
export type AdminV1IssueCreditsBodyInput = z.input<typeof adminV1IssueCreditsBodySchema>
export type AdminV1IssueCreditsBody = z.output<typeof adminV1IssueCreditsBodySchema>
const adminV1IssueCreditsResultSchema = z.object({
success: z.literal(true),
userId: z.string(),
userEmail: z.string().nullable(),
entityType: z.enum(['user', 'organization']),
entityId: z.string(),
amount: z.number(),
newCreditBalance: z.number(),
newUsageLimit: z.number(),
})
export const adminV1IssueCreditsContract = defineRouteContract({
method: 'POST',
path: '/api/v1/admin/credits',
body: adminV1IssueCreditsBodySchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1IssueCreditsResultSchema),
},
})
export type AdminV1ListSubscriptionsResponse = ContractJsonResponse<
typeof adminV1ListSubscriptionsContract
>
export type AdminV1GetSubscriptionResponse = ContractJsonResponse<
typeof adminV1GetSubscriptionContract
>
export type AdminV1CancelSubscriptionResponse = ContractJsonResponse<
typeof adminV1CancelSubscriptionContract
>
@@ -0,0 +1,9 @@
export * from '@/lib/api/contracts/v1/admin/access-control'
export * from '@/lib/api/contracts/v1/admin/billing'
export * from '@/lib/api/contracts/v1/admin/organizations'
export * from '@/lib/api/contracts/v1/admin/outbox'
export * from '@/lib/api/contracts/v1/admin/referral-campaigns'
export * from '@/lib/api/contracts/v1/admin/shared'
export * from '@/lib/api/contracts/v1/admin/users'
export * from '@/lib/api/contracts/v1/admin/workflows'
export * from '@/lib/api/contracts/v1/admin/workspaces'
@@ -0,0 +1,342 @@
import { z } from 'zod'
import { type ContractJsonResponse, defineRouteContract } from '@/lib/api/contracts/types'
import {
adminV1BooleanQuerySchema,
adminV1IdParamsSchema,
adminV1ListResponseSchema,
adminV1PaginationQuerySchema,
adminV1SingleResponseSchema,
adminV1SubscriptionSchema,
} from '@/lib/api/contracts/v1/admin/shared'
export const adminV1OrganizationMemberParamsSchema = adminV1IdParamsSchema.extend({
memberId: z.string().min(1),
})
export const adminV1OrganizationSchema = z.object({
id: z.string(),
name: z.string(),
slug: z.string(),
logo: z.string().nullable(),
orgUsageLimit: z.string().nullable(),
storageUsedBytes: z.number(),
departedMemberUsage: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
})
export const adminV1OrganizationDetailSchema = adminV1OrganizationSchema.extend({
memberCount: z.number(),
subscription: adminV1SubscriptionSchema.nullable(),
})
export const adminV1MemberSchema = z.object({
id: z.string(),
userId: z.string(),
organizationId: z.string(),
role: z.string(),
createdAt: z.string(),
userName: z.string(),
userEmail: z.string(),
})
export const adminV1MemberDetailSchema = adminV1MemberSchema.extend({
currentPeriodCost: z.string(),
currentUsageLimit: z.string().nullable(),
billingBlocked: z.boolean(),
})
export const adminV1OrganizationBillingSummarySchema = z.object({
organizationId: z.string(),
organizationName: z.string(),
subscriptionPlan: z.string(),
subscriptionStatus: z.string(),
totalSeats: z.number(),
usedSeats: z.number(),
availableSeats: z.number(),
totalCurrentUsage: z.number(),
totalUsageLimit: z.number(),
minimumBillingAmount: z.number(),
averageUsagePerMember: z.number(),
usagePercentage: z.number(),
billingPeriodStart: z.string().nullable(),
billingPeriodEnd: z.string().nullable(),
membersOverLimit: z.number(),
membersNearLimit: z.number(),
})
export const adminV1SeatAnalyticsSchema = z.object({
organizationId: z.string(),
organizationName: z.string(),
currentSeats: z.number(),
maxSeats: z.number(),
availableSeats: z.number(),
subscriptionPlan: z.string(),
canAddSeats: z.boolean(),
utilizationRate: z.number(),
})
export const adminV1CreateOrganizationBodySchema = z.object({
name: z.string({ error: 'name is required' }).trim().min(1, { error: 'name is required' }),
ownerId: z.string({ error: 'ownerId is required' }).min(1, { error: 'ownerId is required' }),
slug: z.string().optional(),
})
export const adminV1UpdateOrganizationBodySchema = z.object({
name: z
.string({ error: 'name must be a non-empty string' })
.trim()
.min(1, { error: 'name must be a non-empty string' })
.optional(),
slug: z
.string({ error: 'slug must be a non-empty string' })
.trim()
.min(1, { error: 'slug must be a non-empty string' })
.optional(),
})
export const adminV1AddOrganizationMemberBodySchema = z.object({
userId: z.string({ error: 'userId is required' }).min(1, { error: 'userId is required' }),
role: z.enum(['admin', 'member'], { error: 'role must be "admin" or "member"' }),
})
export const adminV1UpdateOrganizationMemberBodySchema = z.object({
role: z.enum(['admin', 'member'], { error: 'role must be "admin" or "member"' }),
})
export const adminV1RemoveOrganizationMemberQuerySchema = z.object({
skipBillingLogic: adminV1BooleanQuerySchema,
})
export const adminV1UpdateOrganizationBillingBodySchema = z.object({
orgUsageLimit: z
.union([
z
.number({ error: 'orgUsageLimit must be a non-negative number or null' })
.min(0, { error: 'orgUsageLimit must be a non-negative number or null' }),
z.null(),
])
.optional(),
})
export const adminV1TransferOwnershipBodySchema = z.object({
newOwnerUserId: z
.string({ error: 'newOwnerUserId is required' })
.min(1, { error: 'newOwnerUserId is required' }),
currentOwnerUserId: z
.string({ error: 'currentOwnerUserId must be a non-empty string when provided' })
.min(1, { error: 'currentOwnerUserId must be a non-empty string when provided' })
.optional(),
})
const adminV1OrganizationMemberMutationResultSchema = adminV1MemberSchema.extend({
action: z.enum(['created', 'updated', 'already_member']),
billingActions: z.object({
proUsageSnapshotted: z.boolean(),
proCancelledAtPeriodEnd: z.boolean(),
}),
})
const adminV1RemoveOrganizationMemberResultSchema = z.object({
success: z.literal(true),
memberId: z.string(),
userId: z.string(),
billingActions: z.object({
usageCaptured: z.boolean(),
proRestored: z.boolean(),
usageRestored: z.boolean(),
skipBillingLogic: z.boolean(),
}),
})
const adminV1OrganizationBillingUpdateResultSchema = z.object({
success: z.literal(true),
orgUsageLimit: z.string().nullable(),
})
const adminV1TransferOwnershipResultSchema = z.object({
organizationId: z.string(),
currentOwnerUserId: z.string(),
newOwnerUserId: z.string(),
workspacesReassigned: z.number(),
billedAccountReassigned: z.boolean(),
overageMigrated: z.boolean(),
billingBlockInherited: z.boolean(),
})
export const adminV1ListOrganizationsContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/organizations',
query: adminV1PaginationQuerySchema,
response: {
mode: 'json',
schema: adminV1ListResponseSchema(adminV1OrganizationSchema),
},
})
export const adminV1CreateOrganizationContract = defineRouteContract({
method: 'POST',
path: '/api/v1/admin/organizations',
body: adminV1CreateOrganizationBodySchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1OrganizationSchema.extend({ memberId: z.string() })),
},
})
export const adminV1GetOrganizationContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/organizations/[id]',
params: adminV1IdParamsSchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1OrganizationDetailSchema),
},
})
export const adminV1UpdateOrganizationContract = defineRouteContract({
method: 'PATCH',
path: '/api/v1/admin/organizations/[id]',
params: adminV1IdParamsSchema,
body: adminV1UpdateOrganizationBodySchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1OrganizationSchema),
},
})
export const adminV1ListOrganizationMembersContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/organizations/[id]/members',
params: adminV1IdParamsSchema,
query: adminV1PaginationQuerySchema,
response: {
mode: 'json',
schema: adminV1ListResponseSchema(adminV1MemberDetailSchema),
},
})
export const adminV1AddOrganizationMemberContract = defineRouteContract({
method: 'POST',
path: '/api/v1/admin/organizations/[id]/members',
params: adminV1IdParamsSchema,
body: adminV1AddOrganizationMemberBodySchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1OrganizationMemberMutationResultSchema),
},
})
export const adminV1GetOrganizationMemberContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/organizations/[id]/members/[memberId]',
params: adminV1OrganizationMemberParamsSchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1MemberDetailSchema),
},
})
export const adminV1UpdateOrganizationMemberContract = defineRouteContract({
method: 'PATCH',
path: '/api/v1/admin/organizations/[id]/members/[memberId]',
params: adminV1OrganizationMemberParamsSchema,
body: adminV1UpdateOrganizationMemberBodySchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1MemberSchema),
},
})
export const adminV1RemoveOrganizationMemberContract = defineRouteContract({
method: 'DELETE',
path: '/api/v1/admin/organizations/[id]/members/[memberId]',
params: adminV1OrganizationMemberParamsSchema,
query: adminV1RemoveOrganizationMemberQuerySchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1RemoveOrganizationMemberResultSchema),
},
})
export const adminV1GetOrganizationBillingContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/organizations/[id]/billing',
params: adminV1IdParamsSchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1OrganizationBillingSummarySchema),
},
})
export const adminV1UpdateOrganizationBillingContract = defineRouteContract({
method: 'PATCH',
path: '/api/v1/admin/organizations/[id]/billing',
params: adminV1IdParamsSchema,
body: adminV1UpdateOrganizationBillingBodySchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1OrganizationBillingUpdateResultSchema),
},
})
export const adminV1GetOrganizationSeatsContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/organizations/[id]/seats',
params: adminV1IdParamsSchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1SeatAnalyticsSchema),
},
})
export const adminV1TransferOwnershipContract = defineRouteContract({
method: 'POST',
path: '/api/v1/admin/organizations/[id]/transfer-ownership',
params: adminV1IdParamsSchema,
body: adminV1TransferOwnershipBodySchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1TransferOwnershipResultSchema),
},
})
export type AdminV1ListOrganizationsResponse = ContractJsonResponse<
typeof adminV1ListOrganizationsContract
>
export type AdminV1CreateOrganizationResponse = ContractJsonResponse<
typeof adminV1CreateOrganizationContract
>
export type AdminV1GetOrganizationResponse = ContractJsonResponse<
typeof adminV1GetOrganizationContract
>
export type AdminV1UpdateOrganizationResponse = ContractJsonResponse<
typeof adminV1UpdateOrganizationContract
>
export type AdminV1ListOrganizationMembersResponse = ContractJsonResponse<
typeof adminV1ListOrganizationMembersContract
>
export type AdminV1AddOrganizationMemberResponse = ContractJsonResponse<
typeof adminV1AddOrganizationMemberContract
>
export type AdminV1GetOrganizationMemberResponse = ContractJsonResponse<
typeof adminV1GetOrganizationMemberContract
>
export type AdminV1UpdateOrganizationMemberResponse = ContractJsonResponse<
typeof adminV1UpdateOrganizationMemberContract
>
export type AdminV1RemoveOrganizationMemberResponse = ContractJsonResponse<
typeof adminV1RemoveOrganizationMemberContract
>
export type AdminV1GetOrganizationBillingResponse = ContractJsonResponse<
typeof adminV1GetOrganizationBillingContract
>
export type AdminV1UpdateOrganizationBillingResponse = ContractJsonResponse<
typeof adminV1UpdateOrganizationBillingContract
>
export type AdminV1GetOrganizationSeatsResponse = ContractJsonResponse<
typeof adminV1GetOrganizationSeatsContract
>
export type AdminV1TransferOwnershipResponse = ContractJsonResponse<
typeof adminV1TransferOwnershipContract
>
@@ -0,0 +1,93 @@
import { z } from 'zod'
import {
type ContractJsonResponse,
type ContractParams,
type ContractQuery,
type ContractQueryInput,
defineRouteContract,
} from '@/lib/api/contracts/types'
import {
adminV1IdParamsSchema,
adminV1QueryStringSchema,
lastQueryValue,
} from '@/lib/api/contracts/v1/admin/shared'
const adminV1OutboxStatuses = ['pending', 'processing', 'completed', 'dead_letter'] as const
export const adminV1OutboxQuerySchema = z.object({
status: z.preprocess(
lastQueryValue,
z
.enum(adminV1OutboxStatuses, {
error: `Invalid status. Must be one of: ${adminV1OutboxStatuses.join(', ')}`,
})
.optional()
.default('dead_letter')
),
eventType: adminV1QueryStringSchema.transform((value) => value ?? null),
limit: z
.preprocess((value) => {
const queryValue = lastQueryValue(value)
return typeof queryValue === 'string' ? Number.parseInt(queryValue, 10) : queryValue
}, z.number().int().catch(100))
.catch(100)
.transform((limit) => {
if (!Number.isFinite(limit) || limit <= 0) return 100
return Math.min(500, Math.max(1, limit))
}),
})
const adminV1OutboxListResultSchema = z.object({
success: z.literal(true),
filter: z.object({
status: z.enum(adminV1OutboxStatuses),
eventType: z.string().nullable(),
limit: z.number(),
}),
rows: z.array(z.unknown()),
counts: z.array(
z.object({
status: z.string(),
eventType: z.string(),
count: z.number(),
})
),
})
const adminV1OutboxRequeueResultSchema = z.object({
success: z.literal(true),
requeued: z.object({
id: z.string(),
eventType: z.string(),
}),
})
export const adminV1ListOutboxContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/outbox',
query: adminV1OutboxQuerySchema,
response: {
mode: 'json',
schema: adminV1OutboxListResultSchema,
},
})
export const adminV1RequeueOutboxEventContract = defineRouteContract({
method: 'POST',
path: '/api/v1/admin/outbox/[id]/requeue',
params: adminV1IdParamsSchema,
response: {
mode: 'json',
schema: adminV1OutboxRequeueResultSchema,
},
})
export type AdminV1ListOutboxQueryInput = ContractQueryInput<typeof adminV1ListOutboxContract>
export type AdminV1ListOutboxQuery = ContractQuery<typeof adminV1ListOutboxContract>
export type AdminV1RequeueOutboxEventParams = ContractParams<
typeof adminV1RequeueOutboxEventContract
>
export type AdminV1ListOutboxResponse = ContractJsonResponse<typeof adminV1ListOutboxContract>
export type AdminV1RequeueOutboxEventResponse = ContractJsonResponse<
typeof adminV1RequeueOutboxEventContract
>
@@ -0,0 +1,177 @@
import { z } from 'zod'
import {
type ContractBody,
type ContractBodyInput,
type ContractJsonResponse,
type ContractQuery,
type ContractQueryInput,
defineRouteContract,
} from '@/lib/api/contracts/types'
import {
adminV1FutureIsoDateSchema,
adminV1QueryStringSchema,
adminV1SingleResponseSchema,
lastQueryValue,
} from '@/lib/api/contracts/v1/admin/shared'
const adminV1ReferralCampaignDurations = ['once', 'repeating', 'forever'] as const
const adminV1ReferralCampaignAppliesTo = [
'pro',
'team',
'pro_6000',
'pro_25000',
'team_6000',
'team_25000',
] as const
export const adminV1PromoCodeSchema = z.object({
id: z.string(),
code: z.string(),
couponId: z.string(),
name: z.string(),
percentOff: z.number(),
duration: z.string(),
durationInMonths: z.number().nullable(),
appliesToProductIds: z.array(z.string()).nullable(),
maxRedemptions: z.number().nullable(),
expiresAt: z.string().nullable(),
active: z.boolean(),
timesRedeemed: z.number(),
createdAt: z.string(),
})
export const adminV1ReferralCampaignQuerySchema = z.object({
limit: z
.preprocess((value) => {
const queryValue = lastQueryValue(value)
return typeof queryValue === 'string' ? Number.parseInt(queryValue, 10) : queryValue
}, z.number().int().catch(50))
.catch(50)
.transform((limit) => {
if (limit < 1) return 50
return Math.min(limit, 100)
}),
starting_after: adminV1QueryStringSchema,
active: z
.preprocess(lastQueryValue, z.enum(['true', 'false']).optional().catch(undefined))
.transform((active) => (active === undefined ? undefined : active === 'true')),
})
export const adminV1ReferralCampaignBodySchema = z
.object({
name: z
.string({ error: 'name is required and must be a non-empty string' })
.trim()
.min(1, { error: 'name is required and must be a non-empty string' }),
percentOff: z
.number({ error: 'percentOff must be a number between 1 and 100' })
.finite({ error: 'percentOff must be a number between 1 and 100' })
.min(1, { error: 'percentOff must be a number between 1 and 100' })
.max(100, { error: 'percentOff must be a number between 1 and 100' }),
code: z
.union([
z
.string({ error: 'code must be a string or null' })
.trim()
.min(6, { error: 'code must be at least 6 characters' }),
z.null(),
])
.optional(),
duration: z
.enum(adminV1ReferralCampaignDurations, {
error: `duration must be one of: ${adminV1ReferralCampaignDurations.join(', ')}`,
})
.optional()
.default('once'),
durationInMonths: z
.number({
error:
'durationInMonths is required and must be a positive integer when duration is "repeating"',
})
.int({
error:
'durationInMonths is required and must be a positive integer when duration is "repeating"',
})
.min(1, {
error:
'durationInMonths is required and must be a positive integer when duration is "repeating"',
})
.optional(),
maxRedemptions: z
.union([
z
.number({ error: 'maxRedemptions must be a positive integer' })
.int({ error: 'maxRedemptions must be a positive integer' })
.min(1, { error: 'maxRedemptions must be a positive integer' }),
z.null(),
])
.optional(),
expiresAt: z.union([adminV1FutureIsoDateSchema, z.null()]).optional(),
appliesTo: z
.union([
z
.array(z.enum(adminV1ReferralCampaignAppliesTo), {
error: 'appliesTo must be a non-empty array',
})
.nonempty({ error: 'appliesTo must be a non-empty array' }),
z.null(),
])
.optional(),
})
.refine((body) => body.duration !== 'repeating' || body.durationInMonths !== undefined, {
error:
'durationInMonths is required and must be a positive integer when duration is "repeating"',
path: ['durationInMonths'],
})
const adminV1ReferralCampaignListResultSchema = z.object({
data: z.array(adminV1PromoCodeSchema),
hasMore: z.boolean(),
nextCursor: z.string().optional(),
})
export const adminV1ListReferralCampaignsContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/referral-campaigns',
query: adminV1ReferralCampaignQuerySchema,
response: {
mode: 'json',
schema: adminV1ReferralCampaignListResultSchema,
},
})
export const adminV1CreateReferralCampaignContract = defineRouteContract({
method: 'POST',
path: '/api/v1/admin/referral-campaigns',
body: adminV1ReferralCampaignBodySchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1PromoCodeSchema),
},
})
export type AdminV1PromoCode = z.output<typeof adminV1PromoCodeSchema>
export type AdminV1ReferralCampaignDuration = ContractBody<
typeof adminV1CreateReferralCampaignContract
>['duration']
export type AdminV1ReferralCampaignAppliesTo = NonNullable<
ContractBody<typeof adminV1CreateReferralCampaignContract>['appliesTo']
>[number]
export type AdminV1ListReferralCampaignsQueryInput = ContractQueryInput<
typeof adminV1ListReferralCampaignsContract
>
export type AdminV1ListReferralCampaignsQuery = ContractQuery<
typeof adminV1ListReferralCampaignsContract
>
export type AdminV1CreateReferralCampaignBodyInput = ContractBodyInput<
typeof adminV1CreateReferralCampaignContract
>
export type AdminV1CreateReferralCampaignBody = ContractBody<
typeof adminV1CreateReferralCampaignContract
>
export type AdminV1ListReferralCampaignsResponse = ContractJsonResponse<
typeof adminV1ListReferralCampaignsContract
>
export type AdminV1CreateReferralCampaignResponse = ContractJsonResponse<
typeof adminV1CreateReferralCampaignContract
>
@@ -0,0 +1,93 @@
import { z } from 'zod'
export const adminV1DefaultLimit = 50
export const adminV1MaxLimit = 250
export const lastQueryValue = (value: unknown) => (Array.isArray(value) ? value.at(-1) : value)
export const adminV1IdParamsSchema = z.object({
id: z.string().min(1),
})
export const adminV1PaginationQuerySchema = z.object({
limit: z
.preprocess((value) => {
const queryValue = lastQueryValue(value)
return typeof queryValue === 'string' ? Number.parseInt(queryValue, 10) : queryValue
}, z.number().int().catch(adminV1DefaultLimit))
.catch(adminV1DefaultLimit)
.transform((limit) => {
if (limit < 1) return adminV1DefaultLimit
return Math.min(limit, adminV1MaxLimit)
}),
offset: z
.preprocess((value) => {
const queryValue = lastQueryValue(value)
return typeof queryValue === 'string' ? Number.parseInt(queryValue, 10) : queryValue
}, z.number().int().catch(0))
.catch(0)
.transform((offset) => {
if (offset < 0) return 0
return offset
}),
})
export const adminV1BooleanQuerySchema = z
.preprocess(lastQueryValue, z.enum(['true', 'false']).optional().catch(undefined))
.transform((value) => value === 'true')
export const adminV1ExportFormatQuerySchema = z.object({
format: z.preprocess(lastQueryValue, z.enum(['zip', 'json']).catch('zip')),
})
export const adminV1QueryStringSchema = z.preprocess(lastQueryValue, z.string().optional())
export const adminV1FutureIsoDateSchema = z
.string({ error: 'expiresAt must be a valid ISO 8601 date string' })
.refine((value) => !Number.isNaN(Date.parse(value)), {
error: 'expiresAt must be a valid ISO 8601 date string',
})
.refine((value) => new Date(value).getTime() > Date.now(), {
error: 'expiresAt must be in the future',
})
export const adminV1PaginationMetaSchema = z.object({
total: z.number(),
limit: z.number(),
offset: z.number(),
hasMore: z.boolean(),
})
export const adminV1ListResponseSchema = <T extends z.ZodType>(itemSchema: T) =>
z.object({
data: z.array(itemSchema),
pagination: adminV1PaginationMetaSchema,
})
export const adminV1SingleResponseSchema = <T extends z.ZodType>(dataSchema: T) =>
z.object({
data: dataSchema,
})
export const adminV1SubscriptionSchema = z.object({
id: z.string(),
plan: z.string(),
referenceId: z.string(),
stripeCustomerId: z.string().nullable(),
stripeSubscriptionId: z.string().nullable(),
status: z.string().nullable(),
periodStart: z.string().nullable(),
periodEnd: z.string().nullable(),
cancelAtPeriodEnd: z.boolean().nullable(),
seats: z.number().nullable(),
trialStart: z.string().nullable(),
trialEnd: z.string().nullable(),
metadata: z.unknown(),
})
export type AdminV1IdParamsInput = z.input<typeof adminV1IdParamsSchema>
export type AdminV1IdParams = z.output<typeof adminV1IdParamsSchema>
export type AdminV1PaginationQueryInput = z.input<typeof adminV1PaginationQuerySchema>
export type AdminV1PaginationQuery = z.output<typeof adminV1PaginationQuerySchema>
export type AdminV1ExportFormatQueryInput = z.input<typeof adminV1ExportFormatQuerySchema>
export type AdminV1ExportFormatQuery = z.output<typeof adminV1ExportFormatQuerySchema>
@@ -0,0 +1,119 @@
import { z } from 'zod'
import { type ContractJsonResponse, defineRouteContract } from '@/lib/api/contracts/types'
import {
adminV1IdParamsSchema,
adminV1ListResponseSchema,
adminV1PaginationQuerySchema,
adminV1SingleResponseSchema,
adminV1SubscriptionSchema,
} from '@/lib/api/contracts/v1/admin/shared'
export const adminV1UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string(),
emailVerified: z.boolean(),
image: z.string().nullable(),
createdAt: z.string(),
updatedAt: z.string(),
})
export const adminV1UserBillingSchema = z.object({
userId: z.string(),
userName: z.string(),
userEmail: z.string(),
stripeCustomerId: z.string().nullable(),
currentUsageLimit: z.string().nullable(),
currentPeriodCost: z.string(),
lastPeriodCost: z.string().nullable(),
billedOverageThisPeriod: z.string(),
storageUsedBytes: z.number(),
billingBlocked: z.boolean(),
currentPeriodCopilotCost: z.string(),
lastPeriodCopilotCost: z.string().nullable(),
})
export const adminV1UserBillingWithSubscriptionSchema = adminV1UserBillingSchema.extend({
subscriptions: z.array(adminV1SubscriptionSchema),
organizationMemberships: z.array(
z.object({
organizationId: z.string(),
organizationName: z.string(),
role: z.string(),
})
),
})
export const adminV1UpdateUserBillingBodySchema = z.object({
currentUsageLimit: z
.union([
z
.number({ error: 'currentUsageLimit must be a non-negative number or null' })
.min(0, { error: 'currentUsageLimit must be a non-negative number or null' }),
z.null(),
])
.optional(),
billingBlocked: z.boolean({ error: 'billingBlocked must be a boolean' }).optional(),
currentPeriodCost: z
.number({ error: 'currentPeriodCost must be a non-negative number' })
.min(0, { error: 'currentPeriodCost must be a non-negative number' })
.optional(),
reason: z.string().optional(),
})
const adminV1UserBillingUpdateResultSchema = z.object({
success: z.literal(true),
updated: z.array(z.string()),
warnings: z.array(z.string()),
reason: z.string(),
})
export const adminV1ListUsersContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/users',
query: adminV1PaginationQuerySchema,
response: {
mode: 'json',
schema: adminV1ListResponseSchema(adminV1UserSchema),
},
})
export const adminV1GetUserContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/users/[id]',
params: adminV1IdParamsSchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1UserSchema),
},
})
export const adminV1GetUserBillingContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/users/[id]/billing',
params: adminV1IdParamsSchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1UserBillingWithSubscriptionSchema),
},
})
export const adminV1UpdateUserBillingContract = defineRouteContract({
method: 'PATCH',
path: '/api/v1/admin/users/[id]/billing',
params: adminV1IdParamsSchema,
body: adminV1UpdateUserBillingBodySchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1UserBillingUpdateResultSchema),
},
})
export type AdminV1ListUsersResponse = ContractJsonResponse<typeof adminV1ListUsersContract>
export type AdminV1GetUserResponse = ContractJsonResponse<typeof adminV1GetUserContract>
export type AdminV1GetUserBillingResponse = ContractJsonResponse<
typeof adminV1GetUserBillingContract
>
export type AdminV1UpdateUserBillingResponse = ContractJsonResponse<
typeof adminV1UpdateUserBillingContract
>
@@ -0,0 +1,319 @@
import { z } from 'zod'
import { type ContractJsonResponse, defineRouteContract } from '@/lib/api/contracts/types'
import {
adminV1ExportFormatQuerySchema,
adminV1IdParamsSchema,
adminV1ListResponseSchema,
adminV1PaginationQuerySchema,
adminV1SingleResponseSchema,
} from '@/lib/api/contracts/v1/admin/shared'
import { workflowStateSchema } from '@/lib/api/contracts/workflows'
export const adminV1WorkflowVersionParamsSchema = adminV1IdParamsSchema.extend({
versionId: z
.string()
.transform((value) => Number(value))
.refine((value) => Number.isFinite(value) && value >= 1, {
error: 'Invalid version number',
}),
})
export const adminV1WorkflowSchema = 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 adminV1WorkflowDetailSchema = adminV1WorkflowSchema.extend({
blockCount: z.number(),
edgeCount: z.number(),
})
export const adminV1WorkflowVariableSchema = z.object({
id: z.string(),
name: z.string(),
type: z.enum(['string', 'number', 'boolean', 'object', 'array', 'plain']),
value: z.unknown(),
})
export const adminV1WorkflowExportStateSchema = workflowStateSchema.extend({
metadata: z
.object({
name: z.string().optional(),
description: z.string().optional(),
exportedAt: z.string().optional(),
})
.optional(),
variables: z.record(z.string(), adminV1WorkflowVariableSchema).optional(),
})
export const adminV1WorkflowExportPayloadSchema = 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: adminV1WorkflowExportStateSchema,
})
export const adminV1FolderExportPayloadSchema = z.object({
id: z.string(),
name: z.string(),
parentId: z.string().nullable(),
})
export const adminV1WorkspaceExportPayloadSchema = 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: adminV1WorkflowExportPayloadSchema.shape.workflow,
state: adminV1WorkflowExportStateSchema,
})
),
folders: z.array(adminV1FolderExportPayloadSchema),
})
export const adminV1FolderFullExportPayloadSchema = 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: adminV1WorkflowExportPayloadSchema.shape.workflow.omit({ workspaceId: true }),
state: adminV1WorkflowExportStateSchema,
})
),
folders: z.array(adminV1FolderExportPayloadSchema),
})
export const adminV1ImportResultSchema = z.object({
workflowId: z.string(),
name: z.string(),
success: z.boolean(),
error: z.string().optional(),
})
export const adminV1WorkflowImportResponseSchema = z.object({
workflowId: z.string(),
name: z.string(),
success: z.literal(true),
})
export const adminV1DeploymentVersionSchema = 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 adminV1DeployResultSchema = z.object({
isDeployed: z.literal(true),
version: z.number(),
deployedAt: z.string(),
warnings: z.array(z.string()).optional(),
})
export const adminV1UndeployResultSchema = z.object({
isDeployed: z.literal(false),
warnings: z.array(z.string()).optional(),
})
export const adminV1ExportWorkflowsBodySchema = 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 adminV1WorkflowImportBodySchema = 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()),
]),
})
const adminV1DeleteWorkflowResultSchema = z.object({
success: z.literal(true),
workflowId: z.string(),
})
const adminV1WorkflowVersionsResultSchema = z.object({
versions: z.array(adminV1DeploymentVersionSchema),
})
const adminV1ActivateWorkflowVersionResultSchema = z.object({
success: z.literal(true),
version: z.number(),
deployedAt: z.string(),
warnings: z.array(z.string()).optional(),
})
export const adminV1ListWorkflowsContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/workflows',
query: adminV1PaginationQuerySchema,
response: {
mode: 'json',
schema: adminV1ListResponseSchema(adminV1WorkflowSchema),
},
})
export const adminV1GetWorkflowContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/workflows/[id]',
params: adminV1IdParamsSchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1WorkflowDetailSchema),
},
})
export const adminV1DeleteWorkflowContract = defineRouteContract({
method: 'DELETE',
path: '/api/v1/admin/workflows/[id]',
params: adminV1IdParamsSchema,
response: {
mode: 'json',
schema: adminV1DeleteWorkflowResultSchema,
},
})
export const adminV1DeployWorkflowContract = defineRouteContract({
method: 'POST',
path: '/api/v1/admin/workflows/[id]/deploy',
params: adminV1IdParamsSchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1DeployResultSchema),
},
})
export const adminV1UndeployWorkflowContract = defineRouteContract({
method: 'DELETE',
path: '/api/v1/admin/workflows/[id]/deploy',
params: adminV1IdParamsSchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1UndeployResultSchema),
},
})
export const adminV1ListWorkflowVersionsContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/workflows/[id]/versions',
params: adminV1IdParamsSchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1WorkflowVersionsResultSchema),
},
})
export const adminV1ActivateWorkflowVersionContract = defineRouteContract({
method: 'POST',
path: '/api/v1/admin/workflows/[id]/versions/[versionId]/activate',
params: adminV1WorkflowVersionParamsSchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1ActivateWorkflowVersionResultSchema),
},
})
export const adminV1ExportWorkflowContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/workflows/[id]/export',
params: adminV1IdParamsSchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1WorkflowExportPayloadSchema),
},
})
export const adminV1ExportWorkflowsContract = defineRouteContract({
method: 'POST',
path: '/api/v1/admin/workflows/export',
query: adminV1ExportFormatQuerySchema,
body: adminV1ExportWorkflowsBodySchema,
response: {
mode: 'binary',
},
})
export const adminV1ImportWorkflowContract = defineRouteContract({
method: 'POST',
path: '/api/v1/admin/workflows/import',
body: adminV1WorkflowImportBodySchema,
response: {
mode: 'json',
schema: adminV1WorkflowImportResponseSchema,
},
})
export type AdminV1WorkflowVersionParamsInput = z.input<typeof adminV1WorkflowVersionParamsSchema>
export type AdminV1WorkflowVersionParams = z.output<typeof adminV1WorkflowVersionParamsSchema>
export type AdminV1ExportWorkflowsBodyInput = z.input<typeof adminV1ExportWorkflowsBodySchema>
export type AdminV1ExportWorkflowsBody = z.output<typeof adminV1ExportWorkflowsBodySchema>
export type AdminV1WorkflowImportBodyInput = z.input<typeof adminV1WorkflowImportBodySchema>
export type AdminV1WorkflowImportBody = z.output<typeof adminV1WorkflowImportBodySchema>
export type AdminV1Workflow = z.output<typeof adminV1WorkflowSchema>
export type AdminV1WorkflowDetail = z.output<typeof adminV1WorkflowDetailSchema>
export type AdminV1WorkflowVariable = z.output<typeof adminV1WorkflowVariableSchema>
export type AdminV1WorkflowExportState = z.output<typeof adminV1WorkflowExportStateSchema>
export type AdminV1WorkflowExportPayload = z.output<typeof adminV1WorkflowExportPayloadSchema>
export type AdminV1FolderExportPayload = z.output<typeof adminV1FolderExportPayloadSchema>
export type AdminV1WorkspaceExportPayload = z.output<typeof adminV1WorkspaceExportPayloadSchema>
export type AdminV1FolderFullExportPayload = z.output<typeof adminV1FolderFullExportPayloadSchema>
export type AdminV1ImportResult = z.output<typeof adminV1ImportResultSchema>
export type AdminV1WorkflowImportResponseBody = z.output<typeof adminV1WorkflowImportResponseSchema>
export type AdminV1DeploymentVersion = z.output<typeof adminV1DeploymentVersionSchema>
export type AdminV1DeployResult = z.output<typeof adminV1DeployResultSchema>
export type AdminV1UndeployResult = z.output<typeof adminV1UndeployResultSchema>
export type AdminV1ListWorkflowsResponse = ContractJsonResponse<typeof adminV1ListWorkflowsContract>
export type AdminV1GetWorkflowResponse = ContractJsonResponse<typeof adminV1GetWorkflowContract>
export type AdminV1DeleteWorkflowResponse = ContractJsonResponse<
typeof adminV1DeleteWorkflowContract
>
export type AdminV1DeployWorkflowResponse = ContractJsonResponse<
typeof adminV1DeployWorkflowContract
>
export type AdminV1UndeployWorkflowResponse = ContractJsonResponse<
typeof adminV1UndeployWorkflowContract
>
export type AdminV1ListWorkflowVersionsResponse = ContractJsonResponse<
typeof adminV1ListWorkflowVersionsContract
>
export type AdminV1ActivateWorkflowVersionResponse = ContractJsonResponse<
typeof adminV1ActivateWorkflowVersionContract
>
export type AdminV1ExportWorkflowResponse = ContractJsonResponse<
typeof adminV1ExportWorkflowContract
>
export type AdminV1ImportWorkflowResponse = ContractJsonResponse<
typeof adminV1ImportWorkflowContract
>
@@ -0,0 +1,332 @@
import { z } from 'zod'
import { type ContractJsonResponse, defineRouteContract } from '@/lib/api/contracts/types'
import {
adminV1ExportFormatQuerySchema,
adminV1IdParamsSchema,
adminV1ListResponseSchema,
adminV1PaginationQuerySchema,
adminV1QueryStringSchema,
adminV1SingleResponseSchema,
lastQueryValue,
} from '@/lib/api/contracts/v1/admin/shared'
import {
adminV1ImportResultSchema,
adminV1WorkflowSchema,
} from '@/lib/api/contracts/v1/admin/workflows'
import { workspacePermissionSchema } from '@/lib/api/contracts/workspaces'
export const adminV1WorkspaceMemberParamsSchema = adminV1IdParamsSchema.extend({
memberId: z.string().min(1),
})
export const adminV1DeleteWorkspaceMemberQuerySchema = z.object({
userId: z.preprocess(
lastQueryValue,
z
.string({ error: 'userId query parameter is required' })
.min(1, { error: 'userId query parameter is required' })
),
})
export const adminV1WorkspaceImportQuerySchema = z.object({
createFolders: z
.preprocess(lastQueryValue, z.enum(['true', 'false']).catch('true'))
.transform((value) => value !== 'false'),
rootFolderName: adminV1QueryStringSchema,
})
export const adminV1WorkspaceSchema = z.object({
id: z.string(),
name: z.string(),
ownerId: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
})
export const adminV1WorkspaceDetailSchema = adminV1WorkspaceSchema.extend({
workflowCount: z.number(),
folderCount: z.number(),
})
export const adminV1FolderSchema = 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 adminV1WorkspaceMemberSchema = 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 adminV1WorkspaceImportResponseSchema = z.object({
imported: z.number(),
failed: z.number(),
results: z.array(adminV1ImportResultSchema),
})
export const adminV1WorkspaceMemberBodySchema = 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 adminV1UpdateWorkspaceMemberBodySchema = z.object({
permissions: workspacePermissionSchema.refine((value) => value !== null, {
error: 'permissions must be "admin", "write", or "read"',
}),
})
export const adminV1WorkspaceImportBodySchema = 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: [...] }' }
),
})
const adminV1DeleteWorkspaceWorkflowsResultSchema = z.object({
success: z.literal(true),
deleted: z.number(),
})
const adminV1WorkspaceMemberMutationResultSchema = adminV1WorkspaceMemberSchema.extend({
action: z.enum(['created', 'updated', 'already_member']),
})
const adminV1DeleteWorkspaceMemberResultSchema = z.object({
removed: z.literal(true),
userId: z.string(),
workspaceId: z.string(),
})
const adminV1RemoveWorkspaceMemberResultSchema = z.object({
removed: z.literal(true),
memberId: z.string(),
userId: z.string(),
workspaceId: z.string(),
})
export const adminV1ListWorkspacesContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/workspaces',
query: adminV1PaginationQuerySchema,
response: {
mode: 'json',
schema: adminV1ListResponseSchema(adminV1WorkspaceSchema),
},
})
export const adminV1GetWorkspaceContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/workspaces/[id]',
params: adminV1IdParamsSchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1WorkspaceDetailSchema),
},
})
export const adminV1ListWorkspaceWorkflowsContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/workspaces/[id]/workflows',
params: adminV1IdParamsSchema,
query: adminV1PaginationQuerySchema,
response: {
mode: 'json',
schema: adminV1ListResponseSchema(adminV1WorkflowSchema),
},
})
export const adminV1DeleteWorkspaceWorkflowsContract = defineRouteContract({
method: 'DELETE',
path: '/api/v1/admin/workspaces/[id]/workflows',
params: adminV1IdParamsSchema,
response: {
mode: 'json',
schema: adminV1DeleteWorkspaceWorkflowsResultSchema,
},
})
export const adminV1ListWorkspaceFoldersContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/workspaces/[id]/folders',
params: adminV1IdParamsSchema,
query: adminV1PaginationQuerySchema,
response: {
mode: 'json',
schema: adminV1ListResponseSchema(adminV1FolderSchema),
},
})
export const adminV1ExportWorkspaceContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/workspaces/[id]/export',
params: adminV1IdParamsSchema,
query: adminV1ExportFormatQuerySchema,
response: {
mode: 'binary',
},
})
export const adminV1ImportWorkspaceContract = defineRouteContract({
method: 'POST',
path: '/api/v1/admin/workspaces/[id]/import',
params: adminV1IdParamsSchema,
query: adminV1WorkspaceImportQuerySchema,
response: {
mode: 'json',
schema: adminV1WorkspaceImportResponseSchema,
},
})
export const adminV1ListWorkspaceMembersContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/workspaces/[id]/members',
params: adminV1IdParamsSchema,
query: adminV1PaginationQuerySchema,
response: {
mode: 'json',
schema: adminV1ListResponseSchema(adminV1WorkspaceMemberSchema),
},
})
export const adminV1CreateWorkspaceMemberContract = defineRouteContract({
method: 'POST',
path: '/api/v1/admin/workspaces/[id]/members',
params: adminV1IdParamsSchema,
body: adminV1WorkspaceMemberBodySchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1WorkspaceMemberMutationResultSchema),
},
})
export const adminV1DeleteWorkspaceMemberContract = defineRouteContract({
method: 'DELETE',
path: '/api/v1/admin/workspaces/[id]/members',
params: adminV1IdParamsSchema,
query: adminV1DeleteWorkspaceMemberQuerySchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1DeleteWorkspaceMemberResultSchema),
},
})
export const adminV1GetWorkspaceMemberContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/workspaces/[id]/members/[memberId]',
params: adminV1WorkspaceMemberParamsSchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1WorkspaceMemberSchema),
},
})
export const adminV1UpdateWorkspaceMemberContract = defineRouteContract({
method: 'PATCH',
path: '/api/v1/admin/workspaces/[id]/members/[memberId]',
params: adminV1WorkspaceMemberParamsSchema,
body: adminV1UpdateWorkspaceMemberBodySchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1WorkspaceMemberSchema),
},
})
export const adminV1RemoveWorkspaceMemberContract = defineRouteContract({
method: 'DELETE',
path: '/api/v1/admin/workspaces/[id]/members/[memberId]',
params: adminV1WorkspaceMemberParamsSchema,
response: {
mode: 'json',
schema: adminV1SingleResponseSchema(adminV1RemoveWorkspaceMemberResultSchema),
},
})
export const adminV1ExportFolderContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/folders/[id]/export',
params: adminV1IdParamsSchema,
query: adminV1ExportFormatQuerySchema,
response: {
mode: 'binary',
},
})
export type AdminV1WorkspaceMemberParamsInput = z.input<typeof adminV1WorkspaceMemberParamsSchema>
export type AdminV1WorkspaceMemberParams = z.output<typeof adminV1WorkspaceMemberParamsSchema>
export type AdminV1DeleteWorkspaceMemberQueryInput = z.input<
typeof adminV1DeleteWorkspaceMemberQuerySchema
>
export type AdminV1DeleteWorkspaceMemberQuery = z.output<
typeof adminV1DeleteWorkspaceMemberQuerySchema
>
export type AdminV1WorkspaceImportQueryInput = z.input<typeof adminV1WorkspaceImportQuerySchema>
export type AdminV1WorkspaceImportQuery = z.output<typeof adminV1WorkspaceImportQuerySchema>
export type AdminV1WorkspaceMemberBodyInput = z.input<typeof adminV1WorkspaceMemberBodySchema>
export type AdminV1WorkspaceMemberBody = z.output<typeof adminV1WorkspaceMemberBodySchema>
export type AdminV1UpdateWorkspaceMemberBodyInput = z.input<
typeof adminV1UpdateWorkspaceMemberBodySchema
>
export type AdminV1UpdateWorkspaceMemberBody = z.output<
typeof adminV1UpdateWorkspaceMemberBodySchema
>
export type AdminV1WorkspaceImportBodyInput = z.input<typeof adminV1WorkspaceImportBodySchema>
export type AdminV1WorkspaceImportBody = z.output<typeof adminV1WorkspaceImportBodySchema>
export type AdminV1Workspace = z.output<typeof adminV1WorkspaceSchema>
export type AdminV1WorkspaceDetail = z.output<typeof adminV1WorkspaceDetailSchema>
export type AdminV1Folder = z.output<typeof adminV1FolderSchema>
export type AdminV1WorkspaceMember = z.output<typeof adminV1WorkspaceMemberSchema>
export type AdminV1WorkspaceImportResponseBody = z.output<
typeof adminV1WorkspaceImportResponseSchema
>
export type AdminV1ListWorkspacesResponse = ContractJsonResponse<
typeof adminV1ListWorkspacesContract
>
export type AdminV1GetWorkspaceResponse = ContractJsonResponse<typeof adminV1GetWorkspaceContract>
export type AdminV1ListWorkspaceWorkflowsResponse = ContractJsonResponse<
typeof adminV1ListWorkspaceWorkflowsContract
>
export type AdminV1DeleteWorkspaceWorkflowsResponse = ContractJsonResponse<
typeof adminV1DeleteWorkspaceWorkflowsContract
>
export type AdminV1ListWorkspaceFoldersResponse = ContractJsonResponse<
typeof adminV1ListWorkspaceFoldersContract
>
export type AdminV1ImportWorkspaceResponse = ContractJsonResponse<
typeof adminV1ImportWorkspaceContract
>
export type AdminV1ListWorkspaceMembersResponse = ContractJsonResponse<
typeof adminV1ListWorkspaceMembersContract
>
export type AdminV1CreateWorkspaceMemberResponse = ContractJsonResponse<
typeof adminV1CreateWorkspaceMemberContract
>
export type AdminV1DeleteWorkspaceMemberResponse = ContractJsonResponse<
typeof adminV1DeleteWorkspaceMemberContract
>
export type AdminV1GetWorkspaceMemberResponse = ContractJsonResponse<
typeof adminV1GetWorkspaceMemberContract
>
export type AdminV1UpdateWorkspaceMemberResponse = ContractJsonResponse<
typeof adminV1UpdateWorkspaceMemberContract
>
export type AdminV1RemoveWorkspaceMemberResponse = ContractJsonResponse<
typeof adminV1RemoveWorkspaceMemberContract
>
+104
View File
@@ -0,0 +1,104 @@
import { z } from 'zod'
import { defineRouteContract } from '@/lib/api/contracts/types'
const isoDateString = z.string().refine((value) => !Number.isNaN(Date.parse(value)), {
error: 'Invalid date format. Use ISO 8601.',
})
const optionalQueryString = z.preprocess(
(value) => (value === '' ? undefined : value),
z.string().optional()
)
export const v1AuditLogParamsSchema = z.object({
id: z.string().min(1),
})
export const v1ListAuditLogsQuerySchema = z.object({
action: z.string().optional(),
resourceType: z.string().optional(),
resourceId: z.string().optional(),
workspaceId: z.string().optional(),
actorId: z.string().optional(),
startDate: isoDateString.optional(),
endDate: isoDateString.optional(),
includeDeparted: z
.enum(['true', 'false'])
.transform((value) => value === 'true')
.optional()
.default(false),
limit: z.coerce.number().min(1).max(100).optional().default(50),
cursor: z.string().optional(),
})
export type V1ListAuditLogsQuery = z.output<typeof v1ListAuditLogsQuerySchema>
export type V1AuditLogParams = z.output<typeof v1AuditLogParamsSchema>
export const v1AdminAuditLogsQuerySchema = z.object({
action: optionalQueryString,
resourceType: optionalQueryString,
resourceId: optionalQueryString,
workspaceId: optionalQueryString,
actorId: optionalQueryString,
actorEmail: optionalQueryString,
startDate: z.preprocess((value) => (value === '' ? undefined : value), isoDateString.optional()),
endDate: z.preprocess((value) => (value === '' ? undefined : value), isoDateString.optional()),
})
/**
* Generic wrapper used by v1 admin audit-log responses. The `data` and
* `limits` halves are intentionally `z.unknown()` because this proxy returns
* provider-shaped payloads that vary per route family; tightening here would
* require a discriminated union per route, which is tracked as a follow-up.
*
* boundary-policy: this is the "validates nothing" alias form that the audit
* script's `untyped-response` regex doesn't currently catch. Treat any new
* wrapper of this shape the same way and either annotate at the contract use
* site with `// untyped-response: <reason>` or replace with a concrete schema.
*/
const apiResponseWithLimitsSchema = z
.object({
data: z.unknown(),
limits: z.unknown().optional(),
})
.passthrough()
export const v1ListAuditLogsContract = defineRouteContract({
method: 'GET',
path: '/api/v1/audit-logs',
query: v1ListAuditLogsQuerySchema,
response: {
mode: 'json',
schema: apiResponseWithLimitsSchema,
},
})
export const v1GetAuditLogContract = defineRouteContract({
method: 'GET',
path: '/api/v1/audit-logs/[id]',
params: v1AuditLogParamsSchema,
response: {
mode: 'json',
schema: apiResponseWithLimitsSchema,
},
})
export const v1AdminListAuditLogsContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/audit-logs',
query: v1AdminAuditLogsQuerySchema,
response: {
mode: 'json',
schema: apiResponseWithLimitsSchema,
},
})
export const v1AdminGetAuditLogContract = defineRouteContract({
method: 'GET',
path: '/api/v1/admin/audit-logs/[id]',
params: v1AuditLogParamsSchema,
response: {
mode: 'json',
schema: apiResponseWithLimitsSchema,
},
})
+43
View File
@@ -0,0 +1,43 @@
import { z } from 'zod'
import { defineRouteContract } from '@/lib/api/contracts/types'
import { COPILOT_REQUEST_MODES } from '@/lib/copilot/constants'
export const v1CopilotChatBodySchema = z.object({
message: z.string().min(1, 'message is required'),
workflowId: z.string().optional(),
workflowName: z.string().optional(),
chatId: z.string().optional(),
mode: z.enum(COPILOT_REQUEST_MODES).optional().default('agent'),
model: z.string().optional(),
autoExecuteTools: z.boolean().optional().default(true),
timeout: z.number().int().min(1000).max(3_600_000).optional().default(3_600_000),
})
export type V1CopilotChatBody = z.output<typeof v1CopilotChatBodySchema>
const v1CopilotChatToolCallSchema = z.object({
id: z.string(),
name: z.string(),
status: z.string(),
params: z.record(z.string(), z.unknown()).optional(),
// untyped-response: copilot tool result is the user-defined output of an arbitrary tool invocation
result: z.unknown().optional(),
error: z.string().optional(),
durationMs: z.number().optional(),
})
export const v1CopilotChatContract = defineRouteContract({
method: 'POST',
path: '/api/v1/copilot/chat',
body: v1CopilotChatBodySchema,
response: {
mode: 'json',
schema: z.object({
success: z.boolean(),
content: z.string().optional(),
toolCalls: z.array(v1CopilotChatToolCallSchema).optional(),
chatId: z.string().optional(),
error: z.string().optional(),
}),
},
})
+65
View File
@@ -0,0 +1,65 @@
import { z } from 'zod'
import { defineRouteContract } from '@/lib/api/contracts/types'
export const v1FileParamsSchema = z.object({
fileId: z.string({ error: 'File ID is required' }).min(1, 'File ID is required'),
})
export const v1WorkspaceIdQuerySchema = z.object({
workspaceId: z.string().min(1, 'workspaceId query parameter is required'),
})
export const v1UploadFileFormFieldsSchema = z.object({
workspaceId: z.string().min(1, 'workspaceId form field is required'),
})
export type V1FileParams = z.output<typeof v1FileParamsSchema>
export type V1WorkspaceIdQuery = z.output<typeof v1WorkspaceIdQuerySchema>
export type V1UploadFileFormFields = z.output<typeof v1UploadFileFormFieldsSchema>
const v1FilesResponseSchema = z
.object({
success: z.boolean().optional(),
data: z.unknown().optional(),
})
.passthrough()
export const v1ListFilesContract = defineRouteContract({
method: 'GET',
path: '/api/v1/files',
query: v1WorkspaceIdQuerySchema,
response: {
mode: 'json',
schema: v1FilesResponseSchema,
},
})
export const v1UploadFileContract = defineRouteContract({
method: 'POST',
path: '/api/v1/files',
response: {
mode: 'json',
schema: v1FilesResponseSchema,
},
})
export const v1DownloadFileContract = defineRouteContract({
method: 'GET',
path: '/api/v1/files/[fileId]',
params: v1FileParamsSchema,
query: v1WorkspaceIdQuerySchema,
response: {
mode: 'binary',
},
})
export const v1DeleteFileContract = defineRouteContract({
method: 'DELETE',
path: '/api/v1/files/[fileId]',
params: v1FileParamsSchema,
query: v1WorkspaceIdQuerySchema,
response: {
mode: 'json',
schema: v1FilesResponseSchema,
},
})
+8
View File
@@ -0,0 +1,8 @@
export * from '@/lib/api/contracts/v1/admin'
export * from '@/lib/api/contracts/v1/audit-logs'
export * from '@/lib/api/contracts/v1/copilot'
export * from '@/lib/api/contracts/v1/files'
export * from '@/lib/api/contracts/v1/knowledge'
export * from '@/lib/api/contracts/v1/logs'
export * from '@/lib/api/contracts/v1/tables'
export * from '@/lib/api/contracts/v1/workflows'
@@ -0,0 +1,260 @@
import { z } from 'zod'
import {
knowledgeBaseParamsSchema,
knowledgeDocumentParamsSchema,
successResponseSchema,
} from '@/lib/api/contracts/knowledge/shared'
import { defineRouteContract } from '@/lib/api/contracts/types'
import { KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH } from '@/lib/knowledge/constants'
/**
* Public API v1 schemas (`/api/v1/knowledge/**`)
*
* The public API surface intentionally diverges from the in-app contracts:
* - `workspaceId` is REQUIRED (used for tenant scoping + rate-limiting), not
* inferred from session.
* - Chunking config is the simple three-number form (no `strategy` knob).
* - Names/descriptions have explicit length caps.
* - List queries default to sensible values (`enabledFilter='all'`,
* `sortBy='uploadedAt'`, `sortOrder='desc'`).
*
* Embedding model/dimension are fixed server-side and not accepted as input.
*/
/** Simpler chunking config used by the public API (no `strategy`). */
export const v1ChunkingConfigSchema = z.object({
maxSize: z.number().min(100).max(4000).default(1024),
minSize: z.number().min(1).max(2000).default(100),
overlap: z.number().min(0).max(500).default(200),
})
/** GET `/api/v1/knowledge` — list knowledge bases scoped to a workspace. */
export const v1ListKnowledgeBasesQuerySchema = z.object({
workspaceId: z.string().min(1, 'workspaceId query parameter is required'),
})
/** POST `/api/v1/knowledge` — create a knowledge base. */
export const v1CreateKnowledgeBaseBodySchema = z.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
name: z.string().min(1, 'Name is required').max(255, 'Name must be 255 characters or less'),
description: z
.string()
.max(
KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH,
`Description must be ${KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH} characters or less`
)
.optional(),
chunkingConfig: v1ChunkingConfigSchema.optional().default({
maxSize: 1024,
minSize: 100,
overlap: 200,
}),
})
/** GET/DELETE `/api/v1/knowledge/[id]` — workspace scope param. */
export const v1KnowledgeWorkspaceQuerySchema = z.object({
workspaceId: z.string().min(1, 'workspaceId query parameter is required'),
})
/** PUT `/api/v1/knowledge/[id]` — partial update with workspace scope in body. */
export const v1UpdateKnowledgeBaseBodySchema = z
.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
name: z.string().min(1).max(255, 'Name must be 255 characters or less').optional(),
description: z
.string()
.max(
KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH,
`Description must be ${KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH} characters or less`
)
.optional(),
chunkingConfig: z
.object({
maxSize: z.number().min(100).max(4000),
minSize: z.number().min(1).max(2000),
overlap: z.number().min(0).max(500),
})
.optional(),
})
.refine(
(data) =>
data.name !== undefined ||
data.description !== undefined ||
data.chunkingConfig !== undefined,
{ message: 'At least one of name, description, or chunkingConfig must be provided' }
)
/** GET `/api/v1/knowledge/[id]/documents` — list documents (defaults differ from in-app list). */
export const v1ListKnowledgeDocumentsQuerySchema = z.object({
workspaceId: z.string().min(1, 'workspaceId query parameter is required'),
limit: z.coerce.number().int().min(1).max(100).default(50),
offset: z.coerce.number().int().min(0).default(0),
search: z.string().optional(),
enabledFilter: z.enum(['all', 'enabled', 'disabled']).default('all'),
sortBy: z
.enum([
'filename',
'fileSize',
'tokenCount',
'chunkCount',
'uploadedAt',
'processingStatus',
'enabled',
])
.default('uploadedAt'),
sortOrder: z.enum(['asc', 'desc']).default('desc'),
})
/**
* POST `/api/v1/knowledge/search` tag filter — uses display `tagName` (not
* slot) and a default operator. Distinct from the in-app
* `documentTagFilterSchema`, which is slot-based and used for list filtering.
*/
export const v1SearchTagFilterSchema = z.object({
tagName: z.string(),
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(),
})
/** POST `/api/v1/knowledge/search` body. */
export const v1KnowledgeSearchBodySchema = z
.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
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')
.max(20, 'Maximum 20 knowledge base IDs allowed'),
]),
query: z.string().optional(),
topK: z.number().min(1).max(100).default(10),
tagFilters: z.array(v1SearchTagFilterSchema).optional(),
})
.refine(
(data) => {
const hasQuery = data.query && data.query.trim().length > 0
const hasTagFilters = data.tagFilters && data.tagFilters.length > 0
return hasQuery || hasTagFilters
},
{
message: 'Either query or tagFilters must be provided',
}
)
export type V1ListKnowledgeBasesQuery = z.output<typeof v1ListKnowledgeBasesQuerySchema>
export type V1CreateKnowledgeBaseBody = z.output<typeof v1CreateKnowledgeBaseBodySchema>
export type V1KnowledgeWorkspaceQuery = z.output<typeof v1KnowledgeWorkspaceQuerySchema>
export type V1UpdateKnowledgeBaseBody = z.output<typeof v1UpdateKnowledgeBaseBodySchema>
export type V1ListKnowledgeDocumentsQuery = z.output<typeof v1ListKnowledgeDocumentsQuerySchema>
export type V1KnowledgeSearchBody = z.output<typeof v1KnowledgeSearchBodySchema>
const v1KnowledgeApiResponseSchema = successResponseSchema(z.unknown()).passthrough()
export const v1ListKnowledgeBasesContract = defineRouteContract({
method: 'GET',
path: '/api/v1/knowledge',
query: v1ListKnowledgeBasesQuerySchema,
response: {
mode: 'json',
schema: v1KnowledgeApiResponseSchema,
},
})
export const v1CreateKnowledgeBaseContract = defineRouteContract({
method: 'POST',
path: '/api/v1/knowledge',
body: v1CreateKnowledgeBaseBodySchema,
response: {
mode: 'json',
schema: v1KnowledgeApiResponseSchema,
},
})
export const v1GetKnowledgeBaseContract = defineRouteContract({
method: 'GET',
path: '/api/v1/knowledge/[id]',
params: knowledgeBaseParamsSchema,
query: v1KnowledgeWorkspaceQuerySchema,
response: {
mode: 'json',
schema: v1KnowledgeApiResponseSchema,
},
})
export const v1UpdateKnowledgeBaseContract = defineRouteContract({
method: 'PUT',
path: '/api/v1/knowledge/[id]',
params: knowledgeBaseParamsSchema,
body: v1UpdateKnowledgeBaseBodySchema,
response: {
mode: 'json',
schema: v1KnowledgeApiResponseSchema,
},
})
export const v1DeleteKnowledgeBaseContract = defineRouteContract({
method: 'DELETE',
path: '/api/v1/knowledge/[id]',
params: knowledgeBaseParamsSchema,
query: v1KnowledgeWorkspaceQuerySchema,
response: {
mode: 'json',
schema: v1KnowledgeApiResponseSchema,
},
})
export const v1ListKnowledgeDocumentsContract = defineRouteContract({
method: 'GET',
path: '/api/v1/knowledge/[id]/documents',
params: knowledgeBaseParamsSchema,
query: v1ListKnowledgeDocumentsQuerySchema,
response: {
mode: 'json',
schema: v1KnowledgeApiResponseSchema,
},
})
export const v1UploadKnowledgeDocumentContract = defineRouteContract({
method: 'POST',
path: '/api/v1/knowledge/[id]/documents',
params: knowledgeBaseParamsSchema,
response: {
mode: 'json',
schema: v1KnowledgeApiResponseSchema,
},
})
export const v1GetKnowledgeDocumentContract = defineRouteContract({
method: 'GET',
path: '/api/v1/knowledge/[id]/documents/[documentId]',
params: knowledgeDocumentParamsSchema,
query: v1KnowledgeWorkspaceQuerySchema,
response: {
mode: 'json',
schema: v1KnowledgeApiResponseSchema,
},
})
export const v1DeleteKnowledgeDocumentContract = defineRouteContract({
method: 'DELETE',
path: '/api/v1/knowledge/[id]/documents/[documentId]',
params: knowledgeDocumentParamsSchema,
query: v1KnowledgeWorkspaceQuerySchema,
response: {
mode: 'json',
schema: v1KnowledgeApiResponseSchema,
},
})
export const v1KnowledgeSearchContract = defineRouteContract({
method: 'POST',
path: '/api/v1/knowledge/search',
body: v1KnowledgeSearchBodySchema,
response: {
mode: 'json',
schema: v1KnowledgeApiResponseSchema,
},
})
+80
View File
@@ -0,0 +1,80 @@
import { z } from 'zod'
import { booleanQueryFlagSchema } from '@/lib/api/contracts/primitives'
import { defineRouteContract } from '@/lib/api/contracts/types'
export const v1LogParamsSchema = z.object({
id: z.string().min(1),
})
export const v1ExecutionParamsSchema = z.object({
executionId: z.string().min(1),
})
export const v1ListLogsQuerySchema = z.object({
workspaceId: z.string().min(1),
workflowIds: z.string().optional(),
folderIds: z.string().optional(),
triggers: z.string().optional(),
level: z.enum(['info', 'error']).optional(),
startDate: z.string().optional(),
endDate: z.string().optional(),
executionId: z.string().optional(),
minDurationMs: z.coerce.number().optional(),
maxDurationMs: z.coerce.number().optional(),
minCost: z.coerce.number().optional(),
maxCost: z.coerce.number().optional(),
model: z.string().optional(),
details: z.enum(['basic', 'full']).optional().default('basic'),
includeTraceSpans: booleanQueryFlagSchema.optional().default(false),
includeFinalOutput: booleanQueryFlagSchema.optional().default(false),
// Clamp rather than reject: this limit was previously unbounded, so a hard
// .max() would 400 existing consumers passing a larger value. Page size is
// still capped to bound response size and materialization reads.
limit: z.coerce
.number()
.optional()
.default(100)
.transform((v) => Math.min(Math.max(1, Math.trunc(v)), 1000)),
cursor: z.string().optional(),
order: z.enum(['desc', 'asc']).optional().default('desc'),
})
const v1ApiResponseWithLimitsSchema = z
.object({
limits: z.unknown().optional(),
})
.passthrough()
export type V1ListLogsQuery = z.output<typeof v1ListLogsQuerySchema>
export type V1LogParams = z.output<typeof v1LogParamsSchema>
export type V1ExecutionParams = z.output<typeof v1ExecutionParamsSchema>
export const v1ListLogsContract = defineRouteContract({
method: 'GET',
path: '/api/v1/logs',
query: v1ListLogsQuerySchema,
response: {
mode: 'json',
schema: v1ApiResponseWithLimitsSchema,
},
})
export const v1GetLogContract = defineRouteContract({
method: 'GET',
path: '/api/v1/logs/[id]',
params: v1LogParamsSchema,
response: {
mode: 'json',
schema: v1ApiResponseWithLimitsSchema,
},
})
export const v1GetExecutionContract = defineRouteContract({
method: 'GET',
path: '/api/v1/logs/executions/[executionId]',
params: v1ExecutionParamsSchema,
response: {
mode: 'json',
schema: v1ApiResponseWithLimitsSchema,
},
})
@@ -0,0 +1,271 @@
import { isRecordLike } from '@sim/utils/object'
import { z } from 'zod'
import {
createTableBodySchema,
createTableColumnBodySchema,
deleteTableColumnBodySchema,
deleteTableRowsBodySchema,
insertTableRowBodyBaseSchema,
rowAnchorMutexRefine,
rowDataSchema,
tableIdParamsSchema,
tableRowParamsSchema,
tableRowsQueryBaseSchema,
updateRowsByFilterBodySchema,
updateTableColumnBodySchema,
updateTableRowBodySchema,
upsertTableRowBodySchema,
} from '@/lib/api/contracts/tables'
import { defineRouteContract } from '@/lib/api/contracts/types'
import type { Filter, Sort } from '@/lib/table'
import { TABLE_LIMITS } from '@/lib/table/constants'
const domainObjectSchema = <T>() => z.custom<T>(isRecordLike)
const optionalJsonObjectQuerySchema = <T>(label: string) =>
z
.union([z.string(), domainObjectSchema<T>()])
.optional()
.transform((value, ctx): T | undefined => {
if (value === undefined || value === '') return undefined
if (typeof value !== 'string') return value
try {
const parsed: unknown = JSON.parse(value)
if (isRecordLike(parsed)) return parsed as T
} catch {
ctx.addIssue({ code: 'custom', message: `Invalid ${label} JSON` })
return z.NEVER
}
ctx.addIssue({ code: 'custom', message: `${label} must be a JSON object` })
return z.NEVER
})
export const v1TableRowsQuerySchema = tableRowsQueryBaseSchema.omit({ after: true }).extend({
filter: optionalJsonObjectQuerySchema<Filter>('filter'),
sort: optionalJsonObjectQuerySchema<Sort>('sort'),
})
export const v1ListTablesQuerySchema = z.object({
workspaceId: z.string().min(1, 'workspaceId query parameter is required'),
})
export const v1CreateTableBodySchema = createTableBodySchema.omit({
initialRowCount: true,
})
/**
* Public API insert row body — no caller-controlled `position`. Server places
* new rows at the tail; ordering by index is an in-app affordance only.
*/
export const v1InsertTableRowBodySchema = insertTableRowBodyBaseSchema
.omit({ position: true })
.refine(...rowAnchorMutexRefine)
/**
* Public API batch insert body — no `positions`. Same rationale as above.
*/
export const v1BatchInsertTableRowsBodySchema = z.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
rows: z
.array(rowDataSchema)
.min(1, 'At least one row is required')
.max(
TABLE_LIMITS.MAX_BATCH_INSERT_SIZE,
`Cannot insert more than ${TABLE_LIMITS.MAX_BATCH_INSERT_SIZE} rows per batch`
),
})
export const v1CreateTableRowsBodySchema = z.union([
v1BatchInsertTableRowsBodySchema,
v1InsertTableRowBodySchema,
])
export type V1ListTablesQuery = z.output<typeof v1ListTablesQuerySchema>
export type V1TableRowsQuery = z.output<typeof v1TableRowsQuerySchema>
export type V1InsertTableRowBody = z.output<typeof v1InsertTableRowBodySchema>
export type V1BatchInsertTableRowsBody = z.output<typeof v1BatchInsertTableRowsBodySchema>
export type V1CreateTableRowsBody = z.output<typeof v1CreateTableRowsBodySchema>
const successResponseSchema = <T extends z.ZodType>(dataSchema: T) =>
z.object({
success: z.literal(true),
data: dataSchema,
})
const v1TableApiResponseSchema = successResponseSchema(z.unknown()).passthrough()
export const v1ListTablesContract = defineRouteContract({
method: 'GET',
path: '/api/v1/tables',
query: v1ListTablesQuerySchema,
response: {
mode: 'json',
schema: v1TableApiResponseSchema,
},
})
export const v1CreateTableContract = defineRouteContract({
method: 'POST',
path: '/api/v1/tables',
body: v1CreateTableBodySchema,
response: {
mode: 'json',
schema: v1TableApiResponseSchema,
},
})
export const v1GetTableContract = defineRouteContract({
method: 'GET',
path: '/api/v1/tables/[tableId]',
params: tableIdParamsSchema,
query: v1ListTablesQuerySchema,
response: {
mode: 'json',
schema: v1TableApiResponseSchema,
},
})
export const v1DeleteTableContract = defineRouteContract({
method: 'DELETE',
path: '/api/v1/tables/[tableId]',
params: tableIdParamsSchema,
query: v1ListTablesQuerySchema,
response: {
mode: 'json',
schema: v1TableApiResponseSchema,
},
})
export const v1AddTableColumnContract = defineRouteContract({
method: 'POST',
path: '/api/v1/tables/[tableId]/columns',
params: tableIdParamsSchema,
body: createTableColumnBodySchema,
response: {
mode: 'json',
schema: v1TableApiResponseSchema,
},
})
export const v1UpdateTableColumnContract = defineRouteContract({
method: 'PATCH',
path: '/api/v1/tables/[tableId]/columns',
params: tableIdParamsSchema,
body: updateTableColumnBodySchema,
response: {
mode: 'json',
schema: v1TableApiResponseSchema,
},
})
export const v1DeleteTableColumnContract = defineRouteContract({
method: 'DELETE',
path: '/api/v1/tables/[tableId]/columns',
params: tableIdParamsSchema,
body: deleteTableColumnBodySchema,
response: {
mode: 'json',
schema: v1TableApiResponseSchema,
},
})
export const v1ListTableRowsContract = defineRouteContract({
method: 'GET',
path: '/api/v1/tables/[tableId]/rows',
params: tableIdParamsSchema,
query: v1TableRowsQuerySchema,
response: {
mode: 'json',
schema: v1TableApiResponseSchema,
},
})
export const v1CreateTableRowContract = defineRouteContract({
method: 'POST',
path: '/api/v1/tables/[tableId]/rows',
params: tableIdParamsSchema,
body: v1CreateTableRowsBodySchema,
response: {
mode: 'json',
schema: v1TableApiResponseSchema,
},
})
export const v1BatchCreateTableRowsContract = defineRouteContract({
method: 'POST',
path: '/api/v1/tables/[tableId]/rows',
params: tableIdParamsSchema,
body: v1BatchInsertTableRowsBodySchema,
response: {
mode: 'json',
schema: v1TableApiResponseSchema,
},
})
export const v1UpdateRowsByFilterContract = defineRouteContract({
method: 'PUT',
path: '/api/v1/tables/[tableId]/rows',
params: tableIdParamsSchema,
body: updateRowsByFilterBodySchema,
response: {
mode: 'json',
schema: v1TableApiResponseSchema,
},
})
export const v1DeleteTableRowsContract = defineRouteContract({
method: 'DELETE',
path: '/api/v1/tables/[tableId]/rows',
params: tableIdParamsSchema,
body: deleteTableRowsBodySchema,
response: {
mode: 'json',
schema: v1TableApiResponseSchema,
},
})
export const v1GetTableRowContract = defineRouteContract({
method: 'GET',
path: '/api/v1/tables/[tableId]/rows/[rowId]',
params: tableRowParamsSchema,
query: v1ListTablesQuerySchema,
response: {
mode: 'json',
schema: v1TableApiResponseSchema,
},
})
export const v1UpdateTableRowContract = defineRouteContract({
method: 'PATCH',
path: '/api/v1/tables/[tableId]/rows/[rowId]',
params: tableRowParamsSchema,
body: updateTableRowBodySchema,
response: {
mode: 'json',
schema: v1TableApiResponseSchema,
},
})
export const v1DeleteTableRowContract = defineRouteContract({
method: 'DELETE',
path: '/api/v1/tables/[tableId]/rows/[rowId]',
params: tableRowParamsSchema,
query: v1ListTablesQuerySchema,
response: {
mode: 'json',
schema: v1TableApiResponseSchema,
},
})
export const v1UpsertTableRowContract = defineRouteContract({
method: 'POST',
path: '/api/v1/tables/[tableId]/rows/upsert',
params: tableIdParamsSchema,
body: upsertTableRowBodySchema,
response: {
mode: 'json',
schema: v1TableApiResponseSchema,
},
})
+144
View File
@@ -0,0 +1,144 @@
import { z } from 'zod'
import { deploymentVersionMetadataFieldsSchema } from '@/lib/api/contracts/deployments'
import { booleanQueryFlagSchema } from '@/lib/api/contracts/primitives'
import { defineRouteContract } from '@/lib/api/contracts/types'
import { workflowIdParamsSchema } from '@/lib/api/contracts/workflows'
export const v1ListWorkflowsQuerySchema = z.object({
workspaceId: z.string().min(1),
folderId: z.string().optional(),
deployedOnly: booleanQueryFlagSchema.optional().default(false),
limit: z.coerce.number().min(1).max(100).optional().default(50),
cursor: z.string().optional(),
})
export type V1ListWorkflowsQuery = z.output<typeof v1ListWorkflowsQuerySchema>
/**
* Generic wrapper used by v1 admin workflow list/detail responses. `data` is
* the provider-shaped admin payload (varies per route) and `limits` is an
* optional rate-limit envelope; both are intentionally `z.unknown()` here.
* Tightening would require per-route discriminated unions and is tracked as
* a follow-up.
*
* boundary-policy: this is the "validates nothing" alias form that the audit
* script's `untyped-response` regex doesn't currently catch. Treat any new
* wrapper of this shape the same way — either annotate at the contract use
* site with `// untyped-response: <reason>` or replace with a concrete schema.
*/
const v1WorkflowApiResponseWithLimitsSchema = z
.object({
data: z.unknown(),
limits: z.unknown().optional(),
})
.passthrough()
export const v1ListWorkflowsContract = defineRouteContract({
method: 'GET',
path: '/api/v1/workflows',
query: v1ListWorkflowsQuerySchema,
response: {
mode: 'json',
schema: v1WorkflowApiResponseWithLimitsSchema,
},
})
export const v1GetWorkflowContract = defineRouteContract({
method: 'GET',
path: '/api/v1/workflows/[id]',
params: workflowIdParamsSchema,
response: {
mode: 'json',
schema: v1WorkflowApiResponseWithLimitsSchema,
},
})
/**
* Optional version metadata accepted by the v1 deploy endpoint. Field bounds
* are shared with the UI deployment surface via
* {@link deploymentVersionMetadataFieldsSchema}. The route tolerates an
* absent/empty body, so this schema is validated by the handler against
* `parseOptionalJsonBody` instead of being attached to the contract.
*/
export const v1DeployWorkflowBodySchema = z.object({
name: deploymentVersionMetadataFieldsSchema.shape.name,
description: deploymentVersionMetadataFieldsSchema.shape.description,
})
export type V1DeployWorkflowBody = z.input<typeof v1DeployWorkflowBodySchema>
/** Bounded to the Postgres `integer` range of `workflow_deployment_version.version`. */
const deploymentVersionNumberSchema = z
.number()
.int('version must be an integer')
.min(1, 'version must be a positive integer')
.max(2147483647, 'version is out of range')
/**
* Optional rollback target accepted by the v1 rollback endpoint. When
* `version` is omitted the route rolls back to the deployment version that
* precedes the currently active one. Validated by the handler against
* `parseOptionalJsonBody`, so it is not attached to the contract.
*/
export const v1RollbackWorkflowBodySchema = z.object({
version: deploymentVersionNumberSchema.optional(),
})
export type V1RollbackWorkflowBody = z.input<typeof v1RollbackWorkflowBodySchema>
const v1DeploymentStateSchema = z.object({
id: z.string(),
isDeployed: z.boolean(),
deployedAt: z.string().nullable(),
warnings: z.array(z.string()),
})
export const v1DeployWorkflowDataSchema = v1DeploymentStateSchema.extend({
version: z.number().optional(),
})
export type V1DeployWorkflowData = z.output<typeof v1DeployWorkflowDataSchema>
export const v1RollbackWorkflowDataSchema = v1DeploymentStateSchema.extend({
version: z.number(),
})
export type V1RollbackWorkflowData = z.output<typeof v1RollbackWorkflowDataSchema>
export type V1UndeployWorkflowData = z.output<typeof v1DeploymentStateSchema>
const withV1Limits = <T extends z.ZodType>(data: T) =>
z.object({
data,
limits: z.unknown().optional(),
})
export const v1DeployWorkflowContract = defineRouteContract({
method: 'POST',
path: '/api/v1/workflows/[id]/deploy',
params: workflowIdParamsSchema,
response: {
mode: 'json',
schema: withV1Limits(v1DeployWorkflowDataSchema),
},
})
export const v1UndeployWorkflowContract = defineRouteContract({
method: 'DELETE',
path: '/api/v1/workflows/[id]/deploy',
params: workflowIdParamsSchema,
response: {
mode: 'json',
schema: withV1Limits(v1DeploymentStateSchema),
},
})
export const v1RollbackWorkflowContract = defineRouteContract({
method: 'POST',
path: '/api/v1/workflows/[id]/rollback',
params: workflowIdParamsSchema,
response: {
mode: 'json',
schema: withV1Limits(v1RollbackWorkflowDataSchema),
},
})