Merge remote-tracking branch 'origin/main' into pr-2468

# Conflicts:
#	packages/lib/types/document-audit-logs.ts
#	packages/lib/utils/document-audit-logs.ts
#	packages/prisma/schema.prisma
This commit is contained in:
ephraimduncan
2026-07-02 06:48:23 +00:00
584 changed files with 38071 additions and 6339 deletions
+28
View File
@@ -0,0 +1,28 @@
import { z } from 'zod';
/**
* One entry in a CSC sign-time session, pinning the bytes (`documentDataId`)
* whose digest (`hashB64`) was captured at prep time for a given envelope
* item. The `ordinal` is the position of this entry in the session items array
* — it lines up with the position-ordered `signatures/signHash` response per
* CSC v1.0.4.0 §11.9.
*/
export const ZCscSessionItemSchema = z.object({
envelopeItemId: z.string(),
documentDataId: z.string(),
hashB64: z.string(),
ordinal: z.number().int().nonnegative(),
});
/**
* Contract between CSC prep and sign on `CscSession.itemsJson`.
*
* Built at prep time alongside the captured signedAttrs digests. At sign time
* the mutation re-derives each item's digest against the pinned
* `documentDataId` bytes and dispatches one batched `signatures/signHash` per
* recipient.
*/
export const ZCscSessionItemsSchema = z.array(ZCscSessionItemSchema);
export type TCscSessionItem = z.infer<typeof ZCscSessionItemSchema>;
export type TCscSessionItems = z.infer<typeof ZCscSessionItemsSchema>;
+101 -1
View File
@@ -31,6 +31,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
'DOCUMENT_COMPLETED', // When the document is sealed and fully completed.
'DOCUMENT_CREATED', // When the document is created.
'DOCUMENT_DELETED', // When the document is soft deleted.
'DOCUMENT_CANCELLED', // When a privileged member cancels the document.
'DOCUMENT_FIELDS_AUTO_INSERTED', // When a field is auto inserted during send due to default values (radio/dropdown/checkbox).
'DOCUMENT_FIELD_INSERTED', // When a field is inserted (signed/approved/etc) by a recipient.
'DOCUMENT_FIELD_UNINSERTED', // When a field is uninserted by a recipient.
@@ -62,6 +63,12 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
'EXTERNAL_2FA_TOKEN_VERIFY_FAILED',
'EXTERNAL_2FA_TOKEN_CONSUMED',
'EXTERNAL_2FA_TOKEN_REVOKED',
// CSC / TSP signing events.
'DOCUMENT_RECIPIENT_CSC_AUTHENTICATED', // Service-scope OAuth complete; CSC credential persisted.
'DOCUMENT_RECIPIENT_CSC_AUTHENTICATION_FAILED', // Service-scope OAuth completed but TSP returned a blocking error (empty credential list / invalid cert / refused algorithm).
'DOCUMENT_RECIPIENT_CSC_SIGN_REQUESTED', // Recipient clicked Sign; CSC session created with captured per-item hashes.
'DOCUMENT_RECIPIENT_CSC_AUTHORIZED', // Credential-scope OAuth complete; SAD attached to the CSC session.
'DOCUMENT_RECIPIENT_CSC_SIGNED', // TSP returned signatures and they were embedded into the recipient's PDF bytes.
]);
export const ZDocumentAuditLogEmailTypeSchema = z.enum([
@@ -297,6 +304,16 @@ export const ZDocumentAuditLogEventDocumentDeletedSchema = z.object({
}),
});
/**
* Event: Document cancelled.
*/
export const ZDocumentAuditLogEventDocumentCancelledSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CANCELLED),
data: z.object({
reason: z.string().optional(),
}),
});
/**
* Event: Document field inserted.
*/
@@ -550,12 +567,24 @@ export const ZDocumentAuditLogEventDocumentRecipientCompleteSchema = z.object({
});
/**
* Event: Document recipient completed the document (the recipient has fully actioned and completed their required steps for the document).
* Event: Document recipient rejected the document.
*/
export const ZDocumentAuditLogEventDocumentRecipientRejectedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED),
data: ZBaseRecipientDataSchema.extend({
reason: z.string(),
/**
* Whether the rejection was recorded externally on behalf of the recipient
* via the API, rather than by the recipient directly on the platform.
*/
isExternal: z.boolean().optional(),
/**
* The team member the external rejection was recorded on behalf of, when
* the API caller elected a specific member to attribute the action to.
* Absent when the rejection is attributed to the API user/token itself.
*/
onBehalfOfUserEmail: z.string().optional(),
onBehalfOfUserName: z.string().nullable().optional(),
}),
});
@@ -783,6 +812,71 @@ export const ZDocumentAuditLogEventRecipientExpiredSchema = z.object({
}),
});
/**
* Event: Recipient completed CSC service-scope OAuth — credential discovered, certificate persisted.
*/
export const ZDocumentAuditLogEventDocumentRecipientCscAuthenticatedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_CSC_AUTHENTICATED),
data: ZBaseRecipientDataSchema.extend({
providerId: z.string(),
credentialId: z.string(),
signatureAlgorithm: z.string(),
digestAlgorithm: z.string(),
}),
});
/**
* Event: Recipient's CSC service-scope OAuth completed but the TSP returned a blocking error.
*/
export const ZDocumentAuditLogEventDocumentRecipientCscAuthenticationFailedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_CSC_AUTHENTICATION_FAILED),
data: ZBaseRecipientDataSchema.extend({
providerId: z.string(),
reason: z.string(),
}),
});
/**
* Event: Recipient initiated TSP signing — CSC session created with per-item hashes.
*/
export const ZDocumentAuditLogEventDocumentRecipientCscSignRequestedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_CSC_SIGN_REQUESTED),
data: ZBaseRecipientDataSchema.extend({
providerId: z.string(),
credentialId: z.string(),
sessionId: z.string(),
numSignatures: z.number(),
}),
});
/**
* Event: Recipient completed CSC credential-scope OAuth — SAD attached to session.
*/
export const ZDocumentAuditLogEventDocumentRecipientCscAuthorizedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_CSC_AUTHORIZED),
data: ZBaseRecipientDataSchema.extend({
providerId: z.string(),
credentialId: z.string(),
sessionId: z.string(),
sadExpiresAt: z.coerce.date(),
}),
});
/**
* Event: TSP returned signatures and they were embedded into the recipient's PDF bytes.
*/
export const ZDocumentAuditLogEventDocumentRecipientCscSignedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_CSC_SIGNED),
data: ZBaseRecipientDataSchema.extend({
providerId: z.string(),
credentialId: z.string(),
sessionId: z.string(),
numItemsSigned: z.number(),
signatureAlgorithm: z.string(),
digestAlgorithm: z.string(),
}),
});
export const ZDocumentAuditLogBaseSchema = z.object({
id: z.string(),
createdAt: z.date(),
@@ -804,6 +898,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
ZDocumentAuditLogEventDocumentCompletedSchema,
ZDocumentAuditLogEventDocumentCreatedSchema,
ZDocumentAuditLogEventDocumentDeletedSchema,
ZDocumentAuditLogEventDocumentCancelledSchema,
ZDocumentAuditLogEventDocumentMovedToTeamSchema,
ZDocumentAuditLogEventDocumentDelegatedOwnerCreatedSchema,
ZDocumentAuditLogEventDocumentFieldsAutoInsertedSchema,
@@ -837,6 +932,11 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
ZDocumentAuditLogEventExternal2FATokenConsumedSchema,
ZDocumentAuditLogEventExternal2FATokenRevokedSchema,
ZDocumentAuditLogEventRecipientExpiredSchema,
ZDocumentAuditLogEventDocumentRecipientCscAuthenticatedSchema,
ZDocumentAuditLogEventDocumentRecipientCscAuthenticationFailedSchema,
ZDocumentAuditLogEventDocumentRecipientCscSignRequestedSchema,
ZDocumentAuditLogEventDocumentRecipientCscAuthorizedSchema,
ZDocumentAuditLogEventDocumentRecipientCscSignedSchema,
]),
);
+9
View File
@@ -11,6 +11,8 @@ export const ZLicenseClaimSchema = z.object({
hipaa: z.boolean().optional(),
authenticationPortal: z.boolean().optional(),
billing: z.boolean().optional(),
instanceCscSigning: z.boolean().optional(),
cscQesSigning: z.boolean().optional(),
});
/**
@@ -43,6 +45,13 @@ export type TLicenseClaim = z.infer<typeof ZLicenseClaimSchema>;
export type TLicenseRequest = z.infer<typeof ZLicenseRequestSchema>;
export type TLicenseResponse = z.infer<typeof ZLicenseResponseSchema>;
/**
* String-literal union of every flag the licence server can grant. Adding a
* field to `ZLicenseClaimSchema` automatically extends this type so that
* `assertLicensedFor(flag)` and similar helpers stay in sync.
*/
export type LicenseFlag = keyof TLicenseClaim;
/**
* Schema for the cached license data stored in the file.
*/
+1
View File
@@ -20,6 +20,7 @@ export const ZOrganisationSchema = OrganisationSchema.pick({
originalSubscriptionClaimId: true,
teamCount: true,
memberCount: true,
recipientCount: true,
flags: true,
}),
});
+33
View File
@@ -0,0 +1,33 @@
import { z } from 'zod';
/**
* The cryptographic signature tier an envelope is signed at.
*
* - `SES` — Simple Electronic Signature; the default Documenso flow signed
* with the instance-held certificate.
* - `AES` — Advanced Electronic Signature; recipient-bound signing through a
* Cloud Signature Consortium (CSC) Trust Service Provider.
* - `QES` — Qualified Electronic Signature; the eIDAS-qualified variant of
* AES, also recipient-bound through a CSC TSP.
*
* Stored as free-form TEXT on `Envelope.signatureLevel` so the legal-tier
* taxonomy can expand without a DB enum migration. Validation lives here.
*/
export const ZSignatureLevelSchema = z.enum(['SES', 'AES', 'QES']);
export const SignatureLevel = ZSignatureLevelSchema.enum;
export type TSignatureLevel = z.infer<typeof ZSignatureLevelSchema>;
/**
* Whether an envelope's signature level routes through a Cloud Signature
* Consortium Trust Service Provider (`AES` or `QES`). The single branch point
* for TSP-vs-SES runtime divergence — seal handler, download endpoint,
* completion email, and send-time PDF prep all key off this.
*
* Accepts a raw `string` because the Prisma column is TEXT; unknown values
* are conservatively treated as non-TSP so a malformed row can't accidentally
* trigger TSP-only code paths.
*/
export const isTspEnvelope = (envelope: { signatureLevel: string }): boolean =>
envelope.signatureLevel === SignatureLevel.AES || envelope.signatureLevel === SignatureLevel.QES;
+38 -76
View File
@@ -1,7 +1,22 @@
import { ZOrganisationNameSchema } from '@documenso/trpc/server/organisation-router/create-organisation.types';
import type { SubscriptionClaim } from '@prisma/client';
import { z } from 'zod';
/**
* Rate limit window schema.
*
* Example: "5m", "1h", "1d"
*/
export const ZRateLimitWindowSchema = z.string().regex(/^\d+[smhd]$/);
export const ZRateLimitArraySchema = z.array(
z.object({
window: ZRateLimitWindowSchema,
max: z.number().int().positive(),
}),
);
export type TRateLimitArray = z.infer<typeof ZRateLimitArraySchema>;
/**
* README:
* - If you update this you MUST update the `backport-subscription-claims` schema as well.
@@ -37,6 +52,15 @@ export const ZClaimFlagsSchema = z.object({
externalSigning2fa: z.boolean().optional(),
signingReminders: z.boolean().optional(),
cscQesSigning: z.boolean().optional(),
/**
* Controls whether an organisation is prevented from sending emails.
*
* When this is enabled, ALL emails for the organisation are blocked.
*/
disableEmails: z.boolean().optional(),
});
export type TClaimFlags = z.infer<typeof ZClaimFlagsSchema>;
@@ -113,6 +137,15 @@ export const SUBSCRIPTION_CLAIM_FEATURE_FLAGS: Record<
key: 'signingReminders',
label: 'Signing reminders',
},
cscQesSigning: {
key: 'cscQesSigning',
label: 'QES signing',
isEnterprise: true,
},
disableEmails: {
key: 'disableEmails',
label: 'Disable emails',
},
};
export enum INTERNAL_CLAIM_ID {
@@ -124,109 +157,38 @@ export enum INTERNAL_CLAIM_ID {
ENTERPRISE = 'enterprise',
}
export type InternalClaim = Omit<SubscriptionClaim, 'createdAt' | 'updatedAt'>;
export type InternalClaim = Pick<SubscriptionClaim, 'id' | 'name'>;
export type InternalClaims = {
[key in INTERNAL_CLAIM_ID]: InternalClaim;
};
export const internalClaims: InternalClaims = {
/**
* Free plan has no rates and quotas since this may break self-hosters.
*/
[INTERNAL_CLAIM_ID.FREE]: {
id: INTERNAL_CLAIM_ID.FREE,
name: 'Free',
teamCount: 1,
memberCount: 1,
envelopeItemCount: 5,
locked: true,
flags: {},
},
[INTERNAL_CLAIM_ID.INDIVIDUAL]: {
id: INTERNAL_CLAIM_ID.INDIVIDUAL,
name: 'Individual',
teamCount: 1,
memberCount: 1,
envelopeItemCount: 5,
locked: true,
flags: {
unlimitedDocuments: true,
signingReminders: true,
},
},
[INTERNAL_CLAIM_ID.TEAM]: {
id: INTERNAL_CLAIM_ID.TEAM,
name: 'Teams',
teamCount: 1,
memberCount: 5,
envelopeItemCount: 5,
locked: true,
flags: {
unlimitedDocuments: true,
allowCustomBranding: true,
embedSigning: true,
signingReminders: true,
},
},
[INTERNAL_CLAIM_ID.PLATFORM]: {
id: INTERNAL_CLAIM_ID.PLATFORM,
name: 'Platform',
teamCount: 1,
memberCount: 0,
envelopeItemCount: 10,
locked: true,
flags: {
unlimitedDocuments: true,
allowCustomBranding: true,
hidePoweredBy: true,
emailDomains: false,
embedAuthoring: false,
embedAuthoringWhiteLabel: true,
embedSigning: false,
embedSigningWhiteLabel: true,
signingReminders: true,
},
},
[INTERNAL_CLAIM_ID.ENTERPRISE]: {
id: INTERNAL_CLAIM_ID.ENTERPRISE,
name: 'Enterprise',
teamCount: 0,
memberCount: 0,
envelopeItemCount: 10,
locked: true,
flags: {
unlimitedDocuments: true,
allowCustomBranding: true,
hidePoweredBy: true,
emailDomains: true,
embedAuthoring: true,
embedAuthoringWhiteLabel: true,
embedSigning: true,
embedSigningWhiteLabel: true,
cfr21: true,
authenticationPortal: true,
signingReminders: true,
},
},
[INTERNAL_CLAIM_ID.EARLY_ADOPTER]: {
id: INTERNAL_CLAIM_ID.EARLY_ADOPTER,
name: 'Early Adopter',
teamCount: 0,
memberCount: 0,
envelopeItemCount: 5,
locked: true,
flags: {
unlimitedDocuments: true,
allowCustomBranding: true,
hidePoweredBy: true,
embedSigning: true,
embedSigningWhiteLabel: true,
signingReminders: true,
},
},
} as const;
export const ZStripeOrganisationCreateMetadataSchema = z.object({
organisationName: ZOrganisationNameSchema,
userId: z.number(),
});
export type StripeOrganisationCreateMetadata = z.infer<typeof ZStripeOrganisationCreateMetadataSchema>;
+4
View File
@@ -21,6 +21,7 @@ import { mapSecondaryIdToDocumentId, mapSecondaryIdToTemplateId } from '../utils
*/
export const ZWebhookRecipientSchema = z.object({
id: z.number(),
envelopeId: z.string(),
documentId: z.number().nullable(),
templateId: z.number().nullable(),
email: z.string(),
@@ -64,6 +65,7 @@ export const ZWebhookDocumentMetaSchema = z.object({
*/
export const ZWebhookDocumentSchema = z.object({
id: z.number(),
envelopeId: z.string(),
externalId: z.string().nullable(),
userId: z.number(),
authOptions: z.any().nullable(),
@@ -117,6 +119,7 @@ export const mapEnvelopeToWebhookDocumentPayload = (
const mappedRecipients = rawRecipients.map((recipient) => ({
id: recipient.id,
envelopeId: envelope.id,
documentId: envelope.type === EnvelopeType.DOCUMENT ? legacyId : null,
templateId: envelope.type === EnvelopeType.TEMPLATE ? legacyId : null,
email: recipient.email,
@@ -137,6 +140,7 @@ export const mapEnvelopeToWebhookDocumentPayload = (
return {
id: legacyId,
envelopeId: envelope.id,
externalId: envelope.externalId,
userId: envelope.userId,
authOptions: envelope.authOptions,