chore: merge origin/main into pr-2889 (resolve cancelled/expired status conflicts)

This commit is contained in:
ephraimduncan
2026-06-25 11:14:29 +00:00
460 changed files with 33632 additions and 5002 deletions
@@ -0,0 +1,54 @@
-- AlterTable
-- Add the column with a temporary DEFAULT so Postgres backfills existing rows
-- to 'SES' (the only level the instance supported before this migration). The
-- DEFAULT is then dropped so future INSERTs must specify signatureLevel
-- explicitly via resolveSignatureLevel — the column carries no DB-level default
-- by design (see packages/lib/server-only/signature-level/resolve-signature-level.ts).
ALTER TABLE "Envelope" ADD COLUMN "signatureLevel" TEXT NOT NULL DEFAULT 'SES';
ALTER TABLE "Envelope" ALTER COLUMN "signatureLevel" DROP DEFAULT;
-- CreateTable
CREATE TABLE "CscCredential" (
"id" TEXT NOT NULL,
"providerId" TEXT NOT NULL,
"credentialId" TEXT NOT NULL,
"certCache" BYTEA,
"signatureAlgorithm" TEXT NOT NULL,
"keyType" TEXT NOT NULL,
"digestAlgorithm" TEXT NOT NULL,
"keyLenBits" INTEGER,
"signAlgoParams" TEXT,
"serviceTokenCiphertext" BYTEA,
"serviceTokenExpiresAt" TIMESTAMP(3),
"recipientId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CscCredential_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CscSession" (
"id" TEXT NOT NULL,
"envelopeId" TEXT NOT NULL,
"signingTime" TIMESTAMP(3) NOT NULL,
"itemsJson" JSONB NOT NULL,
"encryptedSad" BYTEA,
"sadExpiresAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"recipientId" INTEGER NOT NULL,
CONSTRAINT "CscSession_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "CscCredential_recipientId_key" ON "CscCredential"("recipientId");
-- CreateIndex
CREATE UNIQUE INDEX "CscSession_recipientId_key" ON "CscSession"("recipientId");
-- AddForeignKey
ALTER TABLE "CscCredential" ADD CONSTRAINT "CscCredential_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CscSession" ADD CONSTRAINT "CscSession_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,52 @@
-- AlterTable
-- Add the new columns with temporary defaults to backfill existing rows, then
-- drop the defaults so the columns match the schema (required, no default).
ALTER TABLE "OrganisationClaim" ADD COLUMN "apiQuota" INTEGER,
ADD COLUMN "apiRateLimits" JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN "documentQuota" INTEGER,
ADD COLUMN "documentRateLimits" JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN "emailQuota" INTEGER,
ADD COLUMN "emailRateLimits" JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN "recipientCount" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "OrganisationClaim" ALTER COLUMN "apiRateLimits" DROP DEFAULT,
ALTER COLUMN "documentRateLimits" DROP DEFAULT,
ALTER COLUMN "emailRateLimits" DROP DEFAULT,
ALTER COLUMN "recipientCount" DROP DEFAULT;
-- AlterTable
ALTER TABLE "SubscriptionClaim" ADD COLUMN "apiQuota" INTEGER,
ADD COLUMN "apiRateLimits" JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN "documentQuota" INTEGER,
ADD COLUMN "documentRateLimits" JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN "emailQuota" INTEGER,
ADD COLUMN "emailRateLimits" JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN "recipientCount" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "SubscriptionClaim" ALTER COLUMN "apiRateLimits" DROP DEFAULT,
ALTER COLUMN "documentRateLimits" DROP DEFAULT,
ALTER COLUMN "emailRateLimits" DROP DEFAULT,
ALTER COLUMN "recipientCount" DROP DEFAULT;
-- CreateTable
CREATE TABLE "OrganisationMonthlyStat" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"organisationId" TEXT NOT NULL,
"period" TEXT NOT NULL,
"documentCount" INTEGER NOT NULL DEFAULT 0,
"emailCount" INTEGER NOT NULL DEFAULT 0,
"apiCount" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "OrganisationMonthlyStat_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "OrganisationMonthlyStat_organisationId_idx" ON "OrganisationMonthlyStat"("organisationId");
-- CreateIndex
CREATE UNIQUE INDEX "OrganisationMonthlyStat_organisationId_period_key" ON "OrganisationMonthlyStat"("organisationId", "period");
-- AddForeignKey
ALTER TABLE "OrganisationMonthlyStat" ADD CONSTRAINT "OrganisationMonthlyStat_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "Organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "OrganisationMonthlyStat" ADD COLUMN "emailReports" INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,28 @@
-- CreateEnum
CREATE TYPE "EmailTransportType" AS ENUM ('SMTP_AUTH', 'SMTP_API', 'RESEND', 'MAILCHANNELS');
-- AlterTable
ALTER TABLE "OrganisationClaim" ADD COLUMN "emailTransportId" TEXT;
-- AlterTable
ALTER TABLE "SubscriptionClaim" ADD COLUMN "emailTransportId" TEXT;
-- CreateTable
CREATE TABLE "EmailTransport" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"name" TEXT NOT NULL,
"type" "EmailTransportType" NOT NULL,
"fromName" TEXT NOT NULL,
"fromAddress" TEXT NOT NULL,
"config" TEXT NOT NULL,
CONSTRAINT "EmailTransport_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "SubscriptionClaim" ADD CONSTRAINT "SubscriptionClaim_emailTransportId_fkey" FOREIGN KEY ("emailTransportId") REFERENCES "EmailTransport"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrganisationClaim" ADD CONSTRAINT "OrganisationClaim_emailTransportId_fkey" FOREIGN KEY ("emailTransportId") REFERENCES "EmailTransport"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1 @@
ALTER TYPE "DocumentStatus" ADD VALUE 'CANCELLED';
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Recipient" ADD COLUMN "reminderCount" INTEGER NOT NULL DEFAULT 0;
+150 -3
View File
@@ -256,7 +256,7 @@ model Subscription {
@@index([organisationId])
}
/// @zod.import(["import { ZClaimFlagsSchema } from '@documenso/lib/types/subscription';"])
/// @zod.import(["import { ZClaimFlagsSchema, ZRateLimitArraySchema } from '@documenso/lib/types/subscription';"])
model SubscriptionClaim {
id String @id @default(cuid())
createdAt DateTime @default(now())
@@ -268,11 +268,24 @@ model SubscriptionClaim {
teamCount Int
memberCount Int
envelopeItemCount Int
recipientCount Int
flags Json /// [ClaimFlags] @zod.custom.use(ZClaimFlagsSchema)
documentRateLimits Json /// [RateLimitArray] @zod.custom.use(ZRateLimitArraySchema)
documentQuota Int?
emailRateLimits Json /// [RateLimitArray] @zod.custom.use(ZRateLimitArraySchema)
emailQuota Int?
apiRateLimits Json /// [RateLimitArray] @zod.custom.use(ZRateLimitArraySchema)
apiQuota Int?
emailTransportId String?
emailTransport EmailTransport? @relation(fields: [emailTransportId], references: [id], onDelete: SetNull)
}
/// @zod.import(["import { ZClaimFlagsSchema } from '@documenso/lib/types/subscription';"])
/// @zod.import(["import { ZClaimFlagsSchema, ZRateLimitArraySchema } from '@documenso/lib/types/subscription';"])
model OrganisationClaim {
id String @id
createdAt DateTime @default(now())
@@ -284,8 +297,41 @@ model OrganisationClaim {
teamCount Int
memberCount Int
envelopeItemCount Int
recipientCount Int
flags Json /// [ClaimFlags] @zod.custom.use(ZClaimFlagsSchema)
documentRateLimits Json /// [RateLimitArray] @zod.custom.use(ZRateLimitArraySchema)
documentQuota Int?
emailRateLimits Json /// [RateLimitArray] @zod.custom.use(ZRateLimitArraySchema)
emailQuota Int?
apiRateLimits Json /// [RateLimitArray] @zod.custom.use(ZRateLimitArraySchema)
apiQuota Int?
emailTransportId String?
emailTransport EmailTransport? @relation(fields: [emailTransportId], references: [id], onDelete: SetNull)
}
model OrganisationMonthlyStat {
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organisationId String
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
/// UTC calendar month in `YYYY-MM` form, e.g. "2026-05".
period String
documentCount Int @default(0)
emailCount Int @default(0)
apiCount Int @default(0)
emailReports Int @default(0)
@@unique([organisationId, period])
@@index([organisationId])
}
model Account {
@@ -336,6 +382,7 @@ enum DocumentStatus {
PENDING
COMPLETED
REJECTED
CANCELLED
}
enum DocumentSource {
@@ -383,7 +430,7 @@ enum EnvelopeType {
TEMPLATE
}
/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"])
/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';", "import { ZSignatureLevelSchema } from '@documenso/lib/types/signature-level';"])
model Envelope {
id String @id
secondaryId String @unique
@@ -401,6 +448,8 @@ model Envelope {
source DocumentSource
qrToken String? /// @zod.string.describe("The token for viewing the document using the QR code on the certificate.")
signatureLevel String /// [SignatureLevel] @zod.custom.use(ZSignatureLevelSchema)
internalVersion Int
useLegacyFieldInsertion Boolean @default(false)
@@ -593,6 +642,7 @@ model Recipient {
signedAt DateTime?
lastReminderSentAt DateTime?
nextReminderAt DateTime?
reminderCount Int @default(0)
authOptions Json? /// [RecipientAuthOptions] @zod.custom.use(ZRecipientAuthOptionsSchema)
signingOrder Int? /// @zod.number.describe("The order in which the recipient should sign the document. Only works if the document is set to sequential signing.")
rejectionReason String?
@@ -604,6 +654,9 @@ model Recipient {
fields Field[]
signatures Signature[]
cscCredential CscCredential?
cscSession CscSession?
@@index([token])
@@index([email])
@@index([envelopeId])
@@ -671,6 +724,72 @@ model Signature {
@@index([recipientId])
}
/// Per-recipient cached credential metadata for the configured Cloud Signature
/// Consortium (CSC) provider. Holds the TSP-validated certificate chain and the
/// symmetric-encrypted service-scope access token. One row per recipient on a
/// CSC envelope; absent for SES recipients.
model CscCredential {
id String @id @default(cuid())
providerId String
credentialId String
/// TSP-validated certificate chain (length-prefixed). Nullable until the
/// service-scope OAuth callback successfully retrieves and validates it.
certCache Bytes?
/// Algorithm metadata derived from the CSC `credentials/info` response.
/// Held as flat scalars so each is independently queryable.
signatureAlgorithm String
keyType String
digestAlgorithm String
keyLenBits Int?
signAlgoParams String?
/// Service-scope access token, symmetric-encrypted at the application layer
/// before persistence. Nullable until the service-scope OAuth callback runs.
serviceTokenCiphertext Bytes?
serviceTokenExpiresAt DateTime?
recipientId Int @unique
recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
/// Per-recipient transient sign-time state held across the credential-scope
/// OAuth round-trip. The `recipientId @unique` constraint caps each recipient
/// to at most one in-flight session — re-clicking Sign UPSERTs the row.
/// Cleaned up transitively when the Recipient is deleted.
/// @zod.import(["import { ZCscSessionItemsSchema } from '@documenso/lib/types/csc-session';"])
model CscSession {
id String @id @default(cuid())
/// Denormalised envelope id for ergonomic lookups. No FK relation — cleanup
/// flows through the Recipient cascade.
envelopeId String
/// Pinned at prep time; stable across overlay re-renders so the captured
/// signedAttrs digest matches the bytes the TSP signs.
signingTime DateTime
/// Contract between prep and sign: an array of
/// `{ envelopeItemId, documentDataId, hashB64, ordinal }` per envelope item.
/// `documentDataId` pins the immutable bytes that produced the captured
/// digest so sign-time can re-derive the same hash against the same content.
itemsJson Json /// [CscSessionItems] @zod.custom.use(ZCscSessionItemsSchema)
/// Populated by the credential-scope OAuth callback. Both nullable at row
/// creation; set together when the SAD is exchanged.
encryptedSad Bytes?
sadExpiresAt DateTime?
createdAt DateTime @default(now())
recipientId Int @unique
recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade)
}
model DocumentShareLink {
id Int @id @default(autoincrement())
email String
@@ -714,6 +833,8 @@ model Organisation {
emailDomains EmailDomain[]
organisationEmails OrganisationEmail[]
monthlyStats OrganisationMonthlyStat[]
avatarImage AvatarImage? @relation(fields: [avatarImageId], references: [id], onDelete: SetNull)
ownerUserId Int
@@ -1094,6 +1215,32 @@ model OrganisationEmail {
teamGlobalSettings TeamGlobalSettings[]
}
enum EmailTransportType {
SMTP_AUTH
SMTP_API
RESEND
MAILCHANNELS
}
model EmailTransport {
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String
type EmailTransportType
// Required from-address override (plaintext, non-secret).
fromName String
fromAddress String
// Encrypted JSON blob of the full transport config (secrets + non-secrets).
config String
subscriptionClaims SubscriptionClaim[]
organisationClaims OrganisationClaim[]
}
model OrganisationAuthenticationPortal {
id String @id
organisation Organisation?
+90
View File
@@ -14,6 +14,7 @@ import {
FIELD_SIGNATURE_META_DEFAULT_VALUES,
FIELD_TEXT_META_DEFAULT_VALUES,
} from '@documenso/lib/types/field-meta';
import { SignatureLevel } from '@documenso/lib/types/signature-level';
import { prefixedId } from '@documenso/lib/universal/id';
import type { Team, User } from '@prisma/client';
import { nanoid } from 'nanoid';
@@ -93,6 +94,7 @@ export const seedBlankDocument = async (owner: User, teamId: number, options: Cr
id: prefixedId('envelope'),
secondaryId: documentId.formattedDocumentId,
internalVersion,
signatureLevel: SignatureLevel.SES,
type: EnvelopeType.DOCUMENT,
documentMetaId: documentMeta.id,
source: DocumentSource.DOCUMENT,
@@ -313,6 +315,7 @@ export const seedDraftDocument = async (
id: prefixedId('envelope'),
secondaryId: documentId.formattedDocumentId,
internalVersion,
signatureLevel: SignatureLevel.SES,
type: EnvelopeType.DOCUMENT,
documentMetaId: documentMeta.id,
source: DocumentSource.DOCUMENT,
@@ -407,6 +410,7 @@ export const seedPendingDocument = async (
id: prefixedId('envelope'),
secondaryId: documentId.formattedDocumentId,
internalVersion,
signatureLevel: SignatureLevel.SES,
type: EnvelopeType.DOCUMENT,
documentMetaId: documentMeta.id,
source: DocumentSource.DOCUMENT,
@@ -660,6 +664,7 @@ export const seedCompletedDocument = async (
id: prefixedId('envelope'),
secondaryId: documentId.formattedDocumentId,
internalVersion,
signatureLevel: SignatureLevel.SES,
type: EnvelopeType.DOCUMENT,
documentMetaId: documentMeta.id,
source: DocumentSource.DOCUMENT,
@@ -717,6 +722,91 @@ export const seedCompletedDocument = async (
return document;
};
export const seedCancelledDocument = async (
sender: User,
teamId: number,
recipients: (User | string)[],
options: CreateDocumentOptions = {},
) => {
const { key, createDocumentOptions = {}, internalVersion = 1 } = options;
const documentData = await prisma.documentData.create({
data: {
type: DocumentDataType.BYTES_64,
data: examplePdf,
initialData: examplePdf,
},
});
const documentMeta = await prisma.documentMeta.create({
data: {},
});
const documentId = await incrementDocumentId();
const document = await prisma.envelope.create({
data: {
id: prefixedId('envelope'),
secondaryId: documentId.formattedDocumentId,
internalVersion,
signatureLevel: SignatureLevel.SES,
type: EnvelopeType.DOCUMENT,
documentMetaId: documentMeta.id,
source: DocumentSource.DOCUMENT,
teamId,
title: `[TEST] Document ${key} - Cancelled`,
status: DocumentStatus.CANCELLED,
completedAt: new Date(),
envelopeItems: {
create: {
id: prefixedId('envelope_item'),
title: `[TEST] Document ${key} - Cancelled`,
documentDataId: documentData.id,
order: 1,
},
},
userId: sender.id,
...createDocumentOptions,
},
include: {
envelopeItems: true,
},
});
for (const recipient of recipients) {
const email = typeof recipient === 'string' ? recipient : recipient.email;
const name = typeof recipient === 'string' ? recipient : (recipient.name ?? '');
await prisma.recipient.create({
data: {
email,
name,
token: nanoid(),
readStatus: ReadStatus.OPENED,
sendStatus: SendStatus.SENT,
signingStatus: SigningStatus.NOT_SIGNED,
envelopeId: document.id,
fields: {
create: {
page: 1,
type: FieldType.NAME,
inserted: false,
customText: name,
positionX: new Prisma.Decimal(1),
positionY: new Prisma.Decimal(1),
width: new Prisma.Decimal(1),
height: new Prisma.Decimal(1),
envelopeId: document.id,
envelopeItemId: document.envelopeItems[0].id,
},
},
},
});
}
return document;
};
/**
* Create 5 team documents:
* - Completed document with 2 recipients.
+5
View File
@@ -6,6 +6,7 @@ import { FIELD_META_TEST_FIELDS } from '@documenso/app-tests/constants/field-met
import { OVERFLOW_TEST_FIELDS } from '@documenso/app-tests/constants/field-overflow-pdf';
import { isBase64Image } from '@documenso/lib/constants/signatures';
import { incrementDocumentId, incrementTemplateId } from '@documenso/lib/server-only/envelope/increment-id';
import { SignatureLevel } from '@documenso/lib/types/signature-level';
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
import { DIRECT_TEMPLATE_RECIPIENT_EMAIL, DIRECT_TEMPLATE_RECIPIENT_NAME } from '../../lib/constants/direct-templates';
import { prisma } from '..';
@@ -76,6 +77,7 @@ export const seedDatabase = async () => {
id: prefixedId('envelope'),
secondaryId: documentId.formattedDocumentId,
internalVersion: 1,
signatureLevel: SignatureLevel.SES,
type: EnvelopeType.DOCUMENT,
documentMetaId: documentMeta.id,
source: DocumentSource.DOCUMENT,
@@ -115,6 +117,7 @@ export const seedDatabase = async () => {
id: prefixedId('envelope'),
secondaryId: documentId.formattedDocumentId,
internalVersion: 1,
signatureLevel: SignatureLevel.SES,
type: EnvelopeType.DOCUMENT,
source: DocumentSource.DOCUMENT,
title: `Document ${i}`,
@@ -312,6 +315,7 @@ export const seedAlignmentTestDocument = async ({
id: prefixedId('envelope'),
secondaryId,
internalVersion: 2,
signatureLevel: SignatureLevel.SES,
type,
documentMetaId: documentMeta.id,
source: DocumentSource.DOCUMENT,
@@ -474,6 +478,7 @@ export const seedOverflowTestDocument = async ({
id: prefixedId('envelope'),
secondaryId,
internalVersion: 2,
signatureLevel: SignatureLevel.SES,
type: EnvelopeType.DOCUMENT,
documentMetaId: documentMeta.id,
source: DocumentSource.DOCUMENT,
+4
View File
@@ -6,6 +6,7 @@ import {
DIRECT_TEMPLATE_RECIPIENT_NAME,
} from '@documenso/lib/constants/direct-templates';
import { incrementTemplateId } from '@documenso/lib/server-only/envelope/increment-id';
import { SignatureLevel } from '@documenso/lib/types/signature-level';
import { prefixedId } from '@documenso/lib/universal/id';
import { prisma } from '..';
@@ -57,6 +58,7 @@ export const seedBlankTemplate = async (owner: User, teamId: number, options: Cr
id: prefixedId('envelope'),
secondaryId: templateId.formattedTemplateId,
internalVersion: 1,
signatureLevel: SignatureLevel.SES,
type: EnvelopeType.TEMPLATE,
title: `[TEST] Template ${key}`,
teamId,
@@ -105,6 +107,7 @@ export const seedTemplate = async (options: SeedTemplateOptions) => {
id: prefixedId('envelope'),
secondaryId: templateId.formattedTemplateId,
internalVersion: options.internalVersion ?? 1,
signatureLevel: SignatureLevel.SES,
type: EnvelopeType.TEMPLATE,
title,
envelopeItems: {
@@ -164,6 +167,7 @@ export const seedDirectTemplate = async (options: SeedTemplateOptions) => {
id: prefixedId('envelope'),
secondaryId: templateId.formattedTemplateId,
internalVersion: options.internalVersion ?? 1,
signatureLevel: SignatureLevel.SES,
type: EnvelopeType.TEMPLATE,
title,
envelopeItems: {