d25d482dc2
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
239 lines
9.0 KiB
TypeScript
239 lines
9.0 KiB
TypeScript
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)' })
|
|
)
|