Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09e472327b | ||
|
|
7310b75ee6 | ||
|
|
1bac8417af | ||
|
|
b21e1313d8 | ||
|
|
213f136882 |
@@ -29,7 +29,7 @@ spotless {
|
||||
dependencies {
|
||||
api 'org.springframework.boot:spring-boot-starter-webmvc'
|
||||
api 'org.springframework.boot:spring-boot-starter-aspectj'
|
||||
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20260102.1'
|
||||
api 'com.fathzer:javaluator:3.0.6'
|
||||
api 'com.posthog.java:posthog:1.2.0'
|
||||
api 'org.apache.commons:commons-lang3:3.20.0'
|
||||
@@ -39,7 +39,7 @@ dependencies {
|
||||
api "org.apache.pdfbox:pdfbox-io:$pdfboxVersion"
|
||||
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
|
||||
api "org.apache.pdfbox:preflight:$pdfboxVersion"
|
||||
api 'com.github.junrar:junrar:7.5.7' // RAR archive support for CBR files
|
||||
api 'com.github.junrar:junrar:7.5.8' // RAR archive support for CBR files
|
||||
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||
api 'org.snakeyaml:snakeyaml-engine:3.0.1'
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1"
|
||||
|
||||
@@ -169,7 +169,10 @@ public class PdfMetadataService {
|
||||
.getAuthor();
|
||||
|
||||
if (userService != null) {
|
||||
author = author.replace("username", userService.getCurrentUsername());
|
||||
String username = userService.getCurrentUsername();
|
||||
if (username != null) {
|
||||
author = author.replace("username", username);
|
||||
}
|
||||
}
|
||||
}
|
||||
pdf.getDocumentInformation().setAuthor(author);
|
||||
|
||||
@@ -77,7 +77,7 @@ dependencies {
|
||||
|
||||
implementation 'org.verapdf:validation-model:1.28.2'
|
||||
// CVE-2025-66453: Explicit rhino 1.7.15 to override verapdf's 1.7.13
|
||||
implementation 'org.mozilla:rhino:1.7.15'
|
||||
implementation 'org.mozilla:rhino:1.9.1'
|
||||
|
||||
// veraPDF still uses javax.xml.bind, not the new jakarta namespace
|
||||
implementation 'javax.xml.bind:jaxb-api:2.3.1'
|
||||
@@ -92,7 +92,7 @@ dependencies {
|
||||
exclude group: 'com.google.code.gson', module: 'gson'
|
||||
}
|
||||
// CVE-2022-25647: Explicit gson 2.8.9 to prevent unsafe deserialization (tabula would pull 2.8.7)
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'com.google.code.gson:gson:2.13.2'
|
||||
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
|
||||
implementation 'com.opencsv:opencsv:5.12.0' // https://mvnrepository.com/artifact/com.opencsv/opencsv
|
||||
implementation 'org.apache.poi:poi-ooxml:5.5.1'
|
||||
|
||||
@@ -168,7 +168,8 @@ public class UIDataController {
|
||||
public ResponseEntity<SignData> getSignData() {
|
||||
String username = "";
|
||||
if (userService != null) {
|
||||
username = userService.getCurrentUsername();
|
||||
String currentUsername = userService.getCurrentUsername();
|
||||
username = currentUsername != null ? currentUsername : "";
|
||||
}
|
||||
|
||||
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
|
||||
|
||||
@@ -191,6 +191,30 @@ class PdfMetadataServiceTest {
|
||||
// We don't verify setProducer here to avoid the "Too many actual invocations" error
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetMetadataToPdf_WithProFeaturesAndMissingUsername() {
|
||||
PDDocument testDocument = mock(PDDocument.class);
|
||||
PDDocumentInformation testInfo = mock(PDDocumentInformation.class);
|
||||
when(testDocument.getDocumentInformation()).thenReturn(testInfo);
|
||||
|
||||
PdfMetadataService proService =
|
||||
new PdfMetadataService(
|
||||
applicationProperties, STIRLING_PDF_LABEL, true, userService);
|
||||
|
||||
PdfMetadata testMetadata = PdfMetadata.builder().author("Original Author").build();
|
||||
|
||||
CustomMetadata customMetadata =
|
||||
applicationProperties.getPremium().getProFeatures().getCustomMetadata();
|
||||
when(customMetadata.isAutoUpdateMetadata()).thenReturn(true);
|
||||
when(customMetadata.getAuthor()).thenReturn("Pro Author username");
|
||||
when(userService.getCurrentUsername()).thenReturn(null);
|
||||
|
||||
proService.setMetadataToPdf(testDocument, testMetadata, false);
|
||||
|
||||
// When username is null, the "username" placeholder should not be replaced
|
||||
verify(testInfo).setAuthor("Pro Author username");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetMetadataToPdf_ExistingDocument() {
|
||||
// Create a fresh document
|
||||
|
||||
@@ -49,20 +49,20 @@ dependencies {
|
||||
api 'org.springframework.boot:spring-boot-starter-mail'
|
||||
api 'org.springframework.boot:spring-boot-starter-cache'
|
||||
api 'com.github.ben-manes.caffeine:caffeine'
|
||||
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.42'
|
||||
implementation 'com.bucket4j:bucket4j_jdk17-core:8.15.0'
|
||||
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.43'
|
||||
implementation 'com.bucket4j:bucket4j_jdk17-core:8.16.1'
|
||||
|
||||
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
|
||||
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||
|
||||
api 'io.micrometer:micrometer-registry-prometheus'
|
||||
implementation 'com.unboundid.product.scim2:scim2-sdk-client:4.1.0'
|
||||
implementation 'com.unboundid.product.scim2:scim2-sdk-client:5.0.0'
|
||||
|
||||
api "io.jsonwebtoken:jjwt-api:$jwtVersion"
|
||||
runtimeOnly "io.jsonwebtoken:jjwt-impl:$jwtVersion"
|
||||
runtimeOnly "io.jsonwebtoken:jjwt-jackson:$jwtVersion"
|
||||
runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database
|
||||
runtimeOnly 'org.postgresql:postgresql:42.7.9'
|
||||
runtimeOnly 'org.postgresql:postgresql:42.7.10'
|
||||
constraints {
|
||||
implementation "org.opensaml:opensaml-core:$openSamlVersion"
|
||||
implementation "org.opensaml:opensaml-saml-api:$openSamlVersion"
|
||||
|
||||
+18
@@ -48,11 +48,19 @@ public class SignatureController {
|
||||
* requirements.
|
||||
*/
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated() && !hasAuthority('ROLE_DEMO_USER')")
|
||||
public ResponseEntity<SavedSignatureResponse> saveSignature(
|
||||
@RequestBody SavedSignatureRequest request) {
|
||||
try {
|
||||
String username = userService.getCurrentUsername();
|
||||
|
||||
if ("shared".equals(request.getScope()) && !userService.isCurrentUserAdmin()) {
|
||||
log.warn(
|
||||
"User {} attempted to create shared signature without admin role",
|
||||
username);
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if (request.getDataUrl() == null || request.getDataUrl().isEmpty()) {
|
||||
log.warn("User {} attempted to save signature without dataUrl", username);
|
||||
@@ -76,6 +84,7 @@ public class SignatureController {
|
||||
* signatures.
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("isAuthenticated() && !hasAuthority('ROLE_DEMO_USER')")
|
||||
public ResponseEntity<List<SavedSignatureResponse>> listSignatures() {
|
||||
try {
|
||||
String username = userService.getCurrentUsername();
|
||||
@@ -98,12 +107,21 @@ public class SignatureController {
|
||||
try {
|
||||
String username = userService.getCurrentUsername();
|
||||
String newLabel = body.get("label");
|
||||
boolean isAdmin = userService.isCurrentUserAdmin();
|
||||
|
||||
if (newLabel == null || newLabel.trim().isEmpty()) {
|
||||
log.warn("Invalid label update request");
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
if (signatureService.isSharedSignature(signatureId) && !isAdmin) {
|
||||
log.warn(
|
||||
"User {} attempted to update shared signature {} without admin role",
|
||||
username,
|
||||
signatureId);
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
signatureService.updateSignatureLabel(username, signatureId, newLabel);
|
||||
log.info("User {} updated label for signature {}", username, signatureId);
|
||||
return ResponseEntity.noContent().build();
|
||||
|
||||
+11
-1
@@ -522,7 +522,17 @@ public class UserService implements UserServiceInterface {
|
||||
|
||||
@Override
|
||||
public String getCurrentUsername() {
|
||||
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication == null) {
|
||||
log.debug("No authentication found in security context when resolving username");
|
||||
return null;
|
||||
}
|
||||
|
||||
Object principal = authentication.getPrincipal();
|
||||
if (principal == null) {
|
||||
log.debug("No principal found on authentication object when resolving username");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
return detailsUser.getUsername();
|
||||
|
||||
+6
@@ -249,6 +249,12 @@ public class SignatureService implements PersonalSignatureServiceInterface {
|
||||
throw new FileNotFoundException("Signature metadata not found");
|
||||
}
|
||||
|
||||
public boolean isSharedSignature(String signatureId) {
|
||||
validateFileName(signatureId);
|
||||
Path sharedFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
||||
return Files.exists(sharedFolder.resolve(signatureId + ".json"));
|
||||
}
|
||||
|
||||
private void updateMetadataLabel(Path metadataPath, String newLabel) throws IOException {
|
||||
String metadataJson = Files.readString(metadataPath, StandardCharsets.UTF_8);
|
||||
SavedSignatureResponse sig =
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package stirling.software.proprietary.controller.api;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
import stirling.software.proprietary.service.SignatureService;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SignatureControllerTest {
|
||||
|
||||
@Mock private SignatureService signatureService;
|
||||
@Mock private UserService userService;
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
SignatureController controller = new SignatureController(signatureService, userService);
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveSignatureForbidsSharedScopeForNonAdmin() throws Exception {
|
||||
when(userService.getCurrentUsername()).thenReturn("user1");
|
||||
when(userService.isCurrentUserAdmin()).thenReturn(false);
|
||||
|
||||
mockMvc.perform(
|
||||
post("/api/v1/proprietary/signatures")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(
|
||||
"""
|
||||
{
|
||||
"id": "sig1",
|
||||
"scope": "shared",
|
||||
"dataUrl": "data:image/png;base64,AAAA"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isForbidden());
|
||||
|
||||
verify(signatureService, never()).saveSignature(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateSignatureLabelForbidsSharedSignatureForNonAdmin() throws Exception {
|
||||
when(userService.getCurrentUsername()).thenReturn("user1");
|
||||
when(userService.isCurrentUserAdmin()).thenReturn(false);
|
||||
when(signatureService.isSharedSignature("sig123")).thenReturn(true);
|
||||
|
||||
mockMvc.perform(
|
||||
post("/api/v1/proprietary/signatures/sig123/label")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"label\":\"new label\"}"))
|
||||
.andExpect(status().isForbidden());
|
||||
|
||||
verify(signatureService, never()).updateSignatureLabel(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateSignatureLabelAllowsPersonalSignatureForNonAdmin() throws Exception {
|
||||
when(userService.getCurrentUsername()).thenReturn("user1");
|
||||
when(userService.isCurrentUserAdmin()).thenReturn(false);
|
||||
when(signatureService.isSharedSignature("sig123")).thenReturn(false);
|
||||
|
||||
mockMvc.perform(
|
||||
post("/api/v1/proprietary/signatures/sig123/label")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"label\":\"new label\"}"))
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
verify(signatureService).updateSignatureLabel(eq("user1"), eq("sig123"), eq("new label"));
|
||||
}
|
||||
}
|
||||
+21
@@ -16,6 +16,8 @@ import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
@@ -150,6 +152,25 @@ class UserServiceTest {
|
||||
assertEquals(AuthenticationType.SAML2, captured.getAuthenticationType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCurrentUsernameReturnsNullWhenAuthenticationMissing() {
|
||||
SecurityContextHolder.clearContext();
|
||||
|
||||
assertNull(userService.getCurrentUsername());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCurrentUsernameReturnsUsernameForAuthenticatedPrincipal() {
|
||||
SecurityContextHolder.getContext()
|
||||
.setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(
|
||||
"alice", "n/a", java.util.List.of()));
|
||||
|
||||
assertEquals("alice", userService.getCurrentUsername());
|
||||
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void addApiKeyToUserGeneratesAndPersists() {
|
||||
User user = new User();
|
||||
|
||||
+5
-5
@@ -22,14 +22,14 @@ import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
ext {
|
||||
springBootVersion = "4.0.3"
|
||||
pdfboxVersion = "3.0.6"
|
||||
imageioVersion = "3.13.0"
|
||||
imageioVersion = "3.13.1"
|
||||
lombokVersion = "1.18.42"
|
||||
bouncycastleVersion = "1.83"
|
||||
springSecuritySamlVersion = "7.0.2"
|
||||
openSamlVersion = "4.3.2"
|
||||
commonmarkVersion = "0.27.1"
|
||||
googleJavaFormatVersion = "1.34.1"
|
||||
logback = "1.5.28"
|
||||
logback = "1.5.32"
|
||||
junitPlatformVersion = "1.12.2"
|
||||
modernJavaVersion = 21
|
||||
}
|
||||
@@ -78,7 +78,7 @@ springBoot {
|
||||
|
||||
allprojects {
|
||||
group = 'stirling.software'
|
||||
version = '2.5.3'
|
||||
version = '2.6.0'
|
||||
|
||||
configurations.configureEach {
|
||||
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
||||
@@ -194,8 +194,8 @@ subprojects {
|
||||
// - CVE-2022-25647: gson 2.8.9+ (explicit dependency overrides tabula 2.8.7)
|
||||
// - CVE-2025-66453: rhino 1.7.15 (explicit dependency overrides verapdf 1.7.13)
|
||||
// Fallback strategy force declarations for additional safety:
|
||||
resolutionStrategy.force 'com.google.code.gson:gson:2.8.9'
|
||||
resolutionStrategy.force 'org.mozilla:rhino:1.7.15'
|
||||
resolutionStrategy.force 'com.google.code.gson:gson:2.13.2'
|
||||
resolutionStrategy.force 'org.mozilla:rhino:1.9.1'
|
||||
// CVE-2025-48924: commons-lang3 3.20.0 DoS prevention
|
||||
resolutionStrategy.force 'org.apache.commons:commons-lang3:3.20.0'
|
||||
// CVE-2024-47554: commons-io 2.21.0 DoS prevention
|
||||
|
||||
@@ -235,11 +235,12 @@ RUN java -Djarmode=tools -jar app.jar extract --layers --destination /layers
|
||||
FROM ubuntu:noble AS gs-build
|
||||
ARG GS_VERSION=10.06.0
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/tmp/gs-build \
|
||||
--mount=type=cache,target=/tmp/gs-build,id=gs-build-${TARGETPLATFORM:-local} \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential curl ca-certificates libfontconfig1-dev && rm -rf /var/lib/apt/lists/* && \
|
||||
GS_TAG="gs$(printf '%s' "${GS_VERSION}" | tr -d '.')" && \
|
||||
cd /tmp/gs-build && \
|
||||
rm -rf ghostscript-* && \
|
||||
(test -d "ghostscript-${GS_VERSION}" || curl -fsSL "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/${GS_TAG}/ghostscript-${GS_VERSION}.tar.gz" | tar xz) && \
|
||||
cd "ghostscript-${GS_VERSION}" && \
|
||||
./configure \
|
||||
@@ -257,12 +258,13 @@ FROM ubuntu:noble AS pdf-tools-build
|
||||
ARG QPDF_VERSION=12.3.2
|
||||
ARG IM_VERSION=7.1.2-13
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/tmp/pdf-tools-build \
|
||||
--mount=type=cache,target=/tmp/pdf-tools-build,id=pdf-tools-${TARGETPLATFORM:-local} \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential cmake libssl-dev libjpeg-dev zlib1g-dev curl ca-certificates pkg-config \
|
||||
libpng-dev libtiff-dev libwebp-dev libxml2-dev libfreetype6-dev liblcms2-dev libzip-dev liblqr-1-0-dev \
|
||||
libltdl-dev libtool && rm -rf /var/lib/apt/lists/* && \
|
||||
cd /tmp/pdf-tools-build && \
|
||||
rm -rf qpdf-* ImageMagick-* && \
|
||||
# Build QPDF
|
||||
(test -d "qpdf-${QPDF_VERSION}" || curl -fsSL "https://github.com/qpdf/qpdf/releases/download/v${QPDF_VERSION}/qpdf-${QPDF_VERSION}.tar.gz" | tar xz) && \
|
||||
cd "qpdf-${QPDF_VERSION}" && \
|
||||
|
||||
@@ -253,11 +253,12 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
FROM ubuntu:noble AS gs-build
|
||||
ARG GS_VERSION=10.06.0
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/tmp/gs-build \
|
||||
--mount=type=cache,target=/tmp/gs-build,id=gs-build-${TARGETPLATFORM:-local} \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential curl ca-certificates libfontconfig1-dev && rm -rf /var/lib/apt/lists/* && \
|
||||
GS_TAG="gs$(printf '%s' "${GS_VERSION}" | tr -d '.')" && \
|
||||
cd /tmp/gs-build && \
|
||||
rm -rf ghostscript-* && \
|
||||
(test -d "ghostscript-${GS_VERSION}" || curl -fsSL "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/${GS_TAG}/ghostscript-${GS_VERSION}.tar.gz" | tar xz) && \
|
||||
cd "ghostscript-${GS_VERSION}" && \
|
||||
./configure \
|
||||
@@ -275,12 +276,13 @@ FROM ubuntu:noble AS pdf-tools-build
|
||||
ARG QPDF_VERSION=12.3.2
|
||||
ARG IM_VERSION=7.1.2-13
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/tmp/pdf-tools-build \
|
||||
--mount=type=cache,target=/tmp/pdf-tools-build,id=pdf-tools-${TARGETPLATFORM:-local} \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential cmake libssl-dev libjpeg-dev zlib1g-dev curl ca-certificates pkg-config \
|
||||
libpng-dev libtiff-dev libwebp-dev libxml2-dev libfreetype6-dev liblcms2-dev libzip-dev liblqr-1-0-dev \
|
||||
libltdl-dev libtool && rm -rf /var/lib/apt/lists/* && \
|
||||
cd /tmp/pdf-tools-build && \
|
||||
rm -rf qpdf-* ImageMagick-* && \
|
||||
# Build QPDF
|
||||
(test -d "qpdf-${QPDF_VERSION}" || curl -fsSL "https://github.com/qpdf/qpdf/releases/download/v${QPDF_VERSION}/qpdf-${QPDF_VERSION}.tar.gz" | tar xz) && \
|
||||
cd "qpdf-${QPDF_VERSION}" && \
|
||||
|
||||
@@ -263,7 +263,7 @@ export class SaasBillingService {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new Error('Failed to fetch billing status');
|
||||
throw new Error('Failed to fetch billing status', { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ export class SaasBillingService {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new Error('Failed to open billing portal');
|
||||
throw new Error('Failed to open billing portal', { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,7 +428,7 @@ export class SaasBillingService {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new Error('Failed to create checkout session');
|
||||
throw new Error('Failed to create checkout session', { cause: error });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user