mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 13:20:08 +03:00
Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e556eba832 | ||
|
|
4ea39559c0 | ||
|
|
00d6a6e071 | ||
|
|
cc22deda3f | ||
|
|
1ed7338bb9 | ||
|
|
5dc4ba0204 | ||
|
|
ad95a046ed | ||
|
|
2d0d4443f8 | ||
|
|
656a0ae268 | ||
|
|
f1ee0bdbab | ||
|
|
abc9a08566 | ||
|
|
ab51bcf0f0 | ||
|
|
0ba8b9fcc1 | ||
|
|
e43af565ab | ||
|
|
58326adee4 | ||
|
|
fd9f52d756 | ||
|
|
91f4376371 | ||
|
|
29df488b4e | ||
|
|
e3ff34efd1 |
@@ -1,6 +1,6 @@
|
||||
# Maintainer: Stirling PDF Inc <contact@stirlingpdf.com>
|
||||
pkgname=stirling-pdf-desktop
|
||||
pkgver=2.14.2
|
||||
pkgver=2.14.3
|
||||
pkgrel=1
|
||||
pkgdesc="Locally hosted, web-based PDF manipulation tool (Tauri desktop app, official Stirling PDF Inc build)"
|
||||
arch=('x86_64')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: Stirling PDF Inc <contact@stirlingpdf.com>
|
||||
pkgname=stirling-pdf-server-bin
|
||||
pkgver=2.14.2
|
||||
pkgver=2.14.3
|
||||
pkgrel=1
|
||||
pkgdesc="Locally hosted, web-based PDF manipulation tool (server JAR, prebuilt)"
|
||||
arch=('any')
|
||||
|
||||
@@ -23,13 +23,9 @@ permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
check-pr:
|
||||
if: (github.event_name == 'pull_request' && github.event.action != 'closed') || github.event_name == 'workflow_dispatch'
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_deploy: ${{ steps.decide.outputs.should_deploy }}
|
||||
is_fork: ${{ steps.resolve.outputs.is_fork }}
|
||||
@@ -101,8 +97,8 @@ jobs:
|
||||
echo "allow_fork=${allow_fork:-false}" >> $GITHUB_OUTPUT
|
||||
|
||||
deploy-v2-pr:
|
||||
needs: [pick, check-pr]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
needs: check-pr
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check-pr.outputs.should_deploy == 'true' && (needs.check-pr.outputs.is_fork == 'false' || needs.check-pr.outputs.allow_fork == 'true')
|
||||
# Concurrency control - only one deployment per PR at a time
|
||||
concurrency:
|
||||
@@ -112,10 +108,10 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
# Single source of truth for whether this preview embeds the admin portal:
|
||||
# drives the image build-arg and the deployment comment.
|
||||
BUILD_PORTAL: "true"
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
@@ -187,12 +183,7 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0 # Fetch full history for commit hash detection
|
||||
|
||||
- name: Set up Depot CLI
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Get version number
|
||||
@@ -237,21 +228,9 @@ jobs:
|
||||
echo "Image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Build and push V2 image (Depot)
|
||||
if: env.USE_DEPOT == 'true' && steps.check-image.outputs.exists == 'false'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.16.0
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
context: .
|
||||
file: ./docker/embedded/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push V2 image (Docker fork fallback)
|
||||
if: env.USE_DEPOT != 'true' && steps.check-image.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
- name: Build and push V2 image
|
||||
if: steps.check-image.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/embedded/Dockerfile
|
||||
@@ -259,7 +238,9 @@ jobs:
|
||||
cache-from: type=gha,scope=stirling-pdf-latest
|
||||
cache-to: type=gha,mode=max,scope=stirling-pdf-latest
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
build-args: |
|
||||
VERSION_TAG=v2-alpha
|
||||
BUILD_PORTAL=${{ env.BUILD_PORTAL }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up SSH
|
||||
@@ -290,6 +271,7 @@ jobs:
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/storage:/storage:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||
STIRLING_BILLING_ACCOUNT_LINK_ENABLED: "true"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SECURITY_INITIALLOGIN_USERNAME: "${{ secrets.TEST_LOGIN_USERNAME }}"
|
||||
SECURITY_INITIALLOGIN_PASSWORD: "${{ secrets.TEST_LOGIN_PASSWORD }}"
|
||||
@@ -333,9 +315,70 @@ jobs:
|
||||
# Set port for output
|
||||
echo "v2_port=${V2_PORT}" >> $GITHUB_OUTPUT
|
||||
|
||||
# ---- Storybook preview (only when this PR touches stories/.storybook) ----
|
||||
# Runs inside the same approved-contributor-gated deploy job, so it deploys
|
||||
# under the exact same access rules as the app preview.
|
||||
- name: Detect Storybook changes
|
||||
id: sb-changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
with:
|
||||
list-files: json
|
||||
filters: |
|
||||
storybook:
|
||||
- 'frontend/**/*.stories.@(ts|tsx|mdx)'
|
||||
- 'frontend/**/*.mdx'
|
||||
- 'frontend/.storybook/**'
|
||||
|
||||
- name: Set up Node.js for Storybook
|
||||
if: steps.sb-changes.outputs.storybook == 'true'
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Task for Storybook
|
||||
if: steps.sb-changes.outputs.storybook == 'true'
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
- name: Build and deploy Storybook
|
||||
id: storybook
|
||||
if: steps.sb-changes.outputs.storybook == 'true'
|
||||
env:
|
||||
VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
|
||||
VPS_USER: ${{ secrets.NEW_VPS_USERNAME }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# `prepare` generates the icon set stories import (not committed).
|
||||
task frontend:prepare
|
||||
task frontend:storybook:build
|
||||
PR=${{ needs.check-pr.outputs.pr_number }}
|
||||
# Served at the ROOT of its own port so Storybook's global MSW worker
|
||||
# (/mockServiceWorker.js) resolves. Port = PR + 20000 (bijective, offset
|
||||
# from the app preview's bare-PR-number port).
|
||||
SB_PORT=$((PR + 20000))
|
||||
DIR=/stirling/SB-PR-$PR
|
||||
tar czf storybook.tgz -C frontend/storybook-static .
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
storybook.tgz "$VPS_USER@$VPS_HOST:/tmp/storybook-$PR.tgz"
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T \
|
||||
"$VPS_USER@$VPS_HOST" << ENDSSH
|
||||
set -e
|
||||
rm -rf "$DIR" && mkdir -p "$DIR"
|
||||
tar xzf /tmp/storybook-$PR.tgz -C "$DIR"
|
||||
rm -f /tmp/storybook-$PR.tgz
|
||||
docker rm -f storybook-pr-$PR 2>/dev/null || true
|
||||
docker run -d --name storybook-pr-$PR --restart unless-stopped \
|
||||
-p $SB_PORT:80 -v "$DIR":/usr/share/nginx/html:ro nginx:alpine
|
||||
ENDSSH
|
||||
echo "url=http://$VPS_HOST:$SB_PORT/" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Post V2 deployment URL to PR
|
||||
if: success()
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
SB_URL: ${{ steps.storybook.outputs.url }}
|
||||
SB_FILES: ${{ steps.sb-changes.outputs.storybook_files }}
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -359,12 +402,40 @@ jobs:
|
||||
}
|
||||
|
||||
const deploymentUrl = `http://${{ secrets.NEW_VPS_HOST }}:${v2Port}`;
|
||||
const httpsUrl = `https://${v2Port}.ssl.stirlingpdf.cloud`;
|
||||
|
||||
// Only mention the portal when this image actually embeds it.
|
||||
// Use the direct IP URL - the SSL hostname isn't supported yet.
|
||||
const withPortal = "${{ env.BUILD_PORTAL }}" === "true";
|
||||
const portalNote = withPortal
|
||||
? `🧩 **Admin portal** included - try it at [${deploymentUrl}/portal](${deploymentUrl}/portal).\n\n`
|
||||
: ``;
|
||||
|
||||
// Storybook preview: only present when this PR changed stories/config.
|
||||
const sbUrl = process.env.SB_URL;
|
||||
let storybookNote = "";
|
||||
if (sbUrl) {
|
||||
const files = JSON.parse(process.env.SB_FILES || "[]");
|
||||
const stories = files.filter((f) => /\.stories\.(ts|tsx|mdx)$/.test(f));
|
||||
const config = files.filter((f) => f.startsWith("frontend/.storybook/"));
|
||||
const shorten = (f) =>
|
||||
f.replace(/^frontend\/editor\/src\//, "").replace(/^frontend\//, "");
|
||||
const storyList = stories.map((f) => `- \`${shorten(f)}\``).join("\n");
|
||||
const configList = config.map((f) => `- \`${shorten(f)}\``).join("\n");
|
||||
const summary =
|
||||
`${stories.length} stor${stories.length === 1 ? "y" : "ies"} changed` +
|
||||
(config.length ? ` (+${config.length} config file${config.length === 1 ? "" : "s"})` : "");
|
||||
storybookNote =
|
||||
`📚 **Storybook:** [${sbUrl}](${sbUrl})\n\n` +
|
||||
`<details>\n<summary>${summary}</summary>\n\n` +
|
||||
(storyList ? `**Stories**\n${storyList}\n\n` : "") +
|
||||
(configList ? `**Config**\n${configList}\n` : "") +
|
||||
`</details>\n\n`;
|
||||
}
|
||||
|
||||
const commentBody = `## 🚀 V2 Auto-Deployment Complete!\n\n` +
|
||||
`Your V2 PR with embedded architecture has been deployed!\n\n` +
|
||||
`🔗 **Direct Test URL (non-SSL)** [${deploymentUrl}](${deploymentUrl})\n\n` +
|
||||
`🔐 **Secure HTTPS URL**: [${httpsUrl}](${httpsUrl})\n\n` +
|
||||
portalNote +
|
||||
storybookNote +
|
||||
`_This deployment will be automatically cleaned up when the PR is closed._\n\n` +
|
||||
`🔄 **Auto-deployed** for approved V2 contributors.`;
|
||||
|
||||
@@ -377,8 +448,7 @@ jobs:
|
||||
|
||||
cleanup-v2-deployment:
|
||||
if: github.event.action == 'closed'
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
@@ -460,7 +530,11 @@ jobs:
|
||||
else
|
||||
echo "V2 PR directory not found, nothing to clean up"
|
||||
fi
|
||||
|
||||
|
||||
# Remove this PR's Storybook preview (container + files), if any.
|
||||
docker rm -f storybook-pr-${{ github.event.pull_request.number }} 2>/dev/null || true
|
||||
rm -rf /stirling/SB-PR-${{ github.event.pull_request.number }}
|
||||
|
||||
# Clean up old unused images (older than 2 weeks) but keep recent ones for reuse
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
|
||||
|
||||
@@ -34,12 +34,8 @@ permissions:
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
check-comment:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
if: |
|
||||
@@ -179,15 +175,11 @@ jobs:
|
||||
}
|
||||
|
||||
deploy-pr:
|
||||
needs: [pick, check-comment]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
needs: check-comment
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
@@ -220,9 +212,9 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
@@ -240,12 +232,7 @@ jobs:
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
- name: Set up Depot CLI
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
@@ -254,23 +241,8 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Build and push PR-specific image (Depot)
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.16.0
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
context: .
|
||||
file: ./docker/embedded/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
build-args: |
|
||||
VERSION_TAG=alpha
|
||||
PROTOTYPES_BUILD=${{ needs.check-comment.outputs.enable_prototypes }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push PR-specific image (Docker fork fallback)
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
- name: Build and push PR-specific image
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/embedded/Dockerfile
|
||||
@@ -283,20 +255,9 @@ jobs:
|
||||
PROTOTYPES_BUILD=${{ needs.check-comment.outputs.enable_prototypes }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push engine image (Depot)
|
||||
if: env.USE_DEPOT == 'true' && needs.check-comment.outputs.enable_prototypes == 'true'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.16.0
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
context: ./engine
|
||||
file: ./engine/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:engine-pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push engine image (Docker fork fallback)
|
||||
if: env.USE_DEPOT != 'true' && needs.check-comment.outputs.enable_prototypes == 'true'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
- name: Build and push engine image
|
||||
if: needs.check-comment.outputs.enable_prototypes == 'true'
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: ./engine
|
||||
file: ./engine/Dockerfile
|
||||
@@ -510,8 +471,7 @@ jobs:
|
||||
|
||||
handle-label-commands:
|
||||
if: ${{ github.event.issue.pull_request != null }}
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
|
||||
@@ -2,8 +2,8 @@ name: _runner-pick
|
||||
|
||||
# Tiny reusable workflow that classifies the trigger as either a "fork PR
|
||||
# from an untrusted contributor" or a "trusted commit" so downstream jobs
|
||||
# can pick a runner class without each one duplicating the 200-char gate
|
||||
# expression in their own `runs-on:`.
|
||||
# can trust-gate (skip secret-dependent jobs on forks) without each one
|
||||
# duplicating the gate expression.
|
||||
#
|
||||
# Caller pattern:
|
||||
#
|
||||
@@ -13,12 +13,12 @@ name: _runner-pick
|
||||
#
|
||||
# real-work:
|
||||
# needs: pick
|
||||
# runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-8' }}
|
||||
# if: needs.pick.outputs.is_fork != 'true'
|
||||
# steps: [...]
|
||||
#
|
||||
# Output:
|
||||
# is_fork: "true" when the trigger is a pull_request from a fork or an
|
||||
# untrusted author_association, "false" otherwise.
|
||||
# Outputs:
|
||||
# is_fork: "true" when the trigger is a pull_request from a fork or an
|
||||
# untrusted author_association, "false" otherwise.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -50,21 +50,18 @@ jobs:
|
||||
AUTHOR_ASSOC: ${{ github.event.pull_request.author_association }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
if [ -z "${PR_NUMBER:-}" ]; then
|
||||
# Not a pull_request event at all (push, schedule, workflow_dispatch,
|
||||
# workflow_call from a non-PR trigger) -> trusted by default.
|
||||
echo "is_fork=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
is_fork=false
|
||||
elif [ "${HEAD_REPO_FORK}" = "true" ]; then
|
||||
is_fork=true
|
||||
else
|
||||
case "${AUTHOR_ASSOC}" in
|
||||
OWNER|MEMBER|COLLABORATOR) is_fork=false ;;
|
||||
*) is_fork=true ;;
|
||||
esac
|
||||
fi
|
||||
if [ "${HEAD_REPO_FORK}" = "true" ]; then
|
||||
echo "is_fork=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
case "${AUTHOR_ASSOC}" in
|
||||
OWNER|MEMBER|COLLABORATOR)
|
||||
echo "is_fork=false" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
*)
|
||||
echo "is_fork=true" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "is_fork=${is_fork}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -18,8 +18,6 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
|
||||
@@ -19,14 +19,8 @@ permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
build:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-8' }}
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -47,7 +41,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
@@ -56,9 +50,9 @@ jobs:
|
||||
key: gradle-deps-${{ runner.os }}-jdk-${{ matrix.jdk-version }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Install Task
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Enterprise E2E (Playwright)
|
||||
|
||||
# Enterprise Playwright suite — exercises premium-key gated features (audit,
|
||||
# teams, analytics) plus full OAuth + SAML logins via the Keycloak compose
|
||||
# stacks under testing/compose. Slow and secret-gated, so it runs in three
|
||||
# stacks under testing/compose. Slow and secret-gated, so it runs in four
|
||||
# situations:
|
||||
#
|
||||
# - PRs that touch proprietary / premium / SSO compose / enterprise tests
|
||||
@@ -12,28 +12,14 @@ name: Enterprise E2E (Playwright)
|
||||
# - on a nightly cron schedule (catches Keycloak image drift, license
|
||||
# expiry, upstream proprietary changes),
|
||||
# - manual workflow_dispatch.
|
||||
#
|
||||
# Auto-skipped when secrets.PREMIUM_KEY_ENTERPRISE is missing (forks, dependabot).
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
depot_cores:
|
||||
description: "Depot runner vCPU count (used in runs-on). Override for benchmarking."
|
||||
required: false
|
||||
type: string
|
||||
default: "8"
|
||||
push:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
- cron: "0 4 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
depot_cores:
|
||||
description: "Depot runner vCPU count (used in runs-on). Override for benchmarking."
|
||||
required: false
|
||||
type: string
|
||||
default: "8"
|
||||
|
||||
# No `concurrency:` block here on purpose. When this workflow is called via
|
||||
# workflow_call from build.yml, ${{ github.workflow }}/event_name/pr_number
|
||||
@@ -52,13 +38,16 @@ jobs:
|
||||
|
||||
playwright-e2e-enterprise:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
|
||||
# Skip on fork PRs / untrusted authors: they have no PREMIUM_KEY_ENTERPRISE,
|
||||
# so the suite can't boot premium and would fail. See the header comment.
|
||||
# GitHub reports the skipped reusable workflow as success.
|
||||
if: needs.pick.outputs.is_fork != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
PREMIUM_KEY: ${{ secrets.PREMIUM_KEY_ENTERPRISE }}
|
||||
PREMIUM_ENABLED: "true"
|
||||
SYSTEM_ENABLEANALYTICS: "false"
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
@@ -165,6 +154,8 @@ jobs:
|
||||
wait_for_backend
|
||||
- name: Run enterprise OAuth Playwright tests
|
||||
id: oauth-tests
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results-oauth.json
|
||||
run: task e2e:enterprise -- --grep "OAuth"
|
||||
- name: Stop backend + tear down OAuth Keycloak
|
||||
if: always()
|
||||
@@ -238,6 +229,8 @@ jobs:
|
||||
wait_for_backend
|
||||
- name: Run enterprise SAML Playwright tests
|
||||
id: saml-tests
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results-saml.json
|
||||
run: task e2e:enterprise -- --grep "SAML"
|
||||
- name: Stop backend + tear down SAML Keycloak
|
||||
if: always()
|
||||
@@ -268,6 +261,8 @@ jobs:
|
||||
wait_for_backend
|
||||
- name: Run enterprise feature Playwright tests
|
||||
id: feature-tests
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results-feature.json
|
||||
run: task e2e:enterprise -- --grep "Enterprise license"
|
||||
- name: Print backend log on failure
|
||||
if: failure()
|
||||
@@ -280,10 +275,23 @@ jobs:
|
||||
run: |
|
||||
source /tmp/helpers.sh
|
||||
stop_backend
|
||||
- name: Flag flaky tests
|
||||
# Runs regardless of the test outcomes: a flaky test (passed on retry)
|
||||
# leaves its step green, so this is the only place it surfaces. Merges
|
||||
# all three phase reports (some may be absent if an earlier phase hard-
|
||||
# failed and skipped the rest). Emits ::warning:: annotations + a job
|
||||
# summary; never fails the job.
|
||||
if: always()
|
||||
working-directory: frontend
|
||||
run: >
|
||||
npx tsx editor/scripts/report-flaky-tests.mts
|
||||
"${{ github.workspace }}/frontend/playwright-report/results-oauth.json"
|
||||
"${{ github.workspace }}/frontend/playwright-report/results-saml.json"
|
||||
"${{ github.workspace }}/frontend/playwright-report/results-feature.json"
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-report-enterprise-${{ github.run_id }}
|
||||
path: frontend/editor/playwright-report/
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
@@ -147,7 +147,6 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
uses: ./.github/workflows/test-build-docker.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
|
||||
@@ -11,8 +11,6 @@ permissions:
|
||||
jobs:
|
||||
check-licence:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
|
||||
@@ -10,14 +10,8 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
check-generate-openapi-docs:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
@@ -34,7 +28,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
@@ -43,9 +37,9 @@ jobs:
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Install Task
|
||||
|
||||
@@ -13,17 +13,24 @@ name: Aggregate backend coverage
|
||||
# producers themselves
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
frontend-validation-result:
|
||||
description: Result of the frontend-validation producer job
|
||||
required: false
|
||||
type: string
|
||||
default: skipped
|
||||
playwright-e2e-live-result:
|
||||
description: Result of the playwright-e2e-live producer job
|
||||
required: false
|
||||
type: string
|
||||
default: skipped
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
aggregate:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
@@ -40,7 +47,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
@@ -49,9 +56,9 @@ jobs:
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Set up Python
|
||||
@@ -196,9 +203,9 @@ jobs:
|
||||
# --------------------------------------------------------------
|
||||
- name: Download vitest coverage artifact
|
||||
# frontend-validation uploads as `frontend-coverage`. Tolerate
|
||||
# absence so a backend-only PR still produces the matrix with
|
||||
# just backend rows populated.
|
||||
if: always()
|
||||
# absence on backend-only runs by skipping the download entirely
|
||||
# when the producer job was not part of this workflow run.
|
||||
if: inputs.frontend-validation-result == 'success'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0
|
||||
with:
|
||||
name: frontend-coverage
|
||||
@@ -206,12 +213,12 @@ jobs:
|
||||
continue-on-error: true
|
||||
|
||||
- name: Download Playwright frontend coverage artifact
|
||||
# e2e-live uploads as `playwright-frontend-coverage-<run_id>`.
|
||||
# Same tolerance as vitest - matrix script handles missing inputs.
|
||||
if: always()
|
||||
# e2e-live uploads the artifact with a stable name. Skip the
|
||||
# download entirely when the producer job did not run.
|
||||
if: inputs.playwright-e2e-live-result == 'success'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0
|
||||
with:
|
||||
name: playwright-frontend-coverage-${{ github.run_id }}
|
||||
name: playwright-frontend-coverage
|
||||
path: matrix-inputs/playwright/
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
@@ -12,15 +12,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
migration-test:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-8' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
@@ -37,7 +31,7 @@ jobs:
|
||||
distribution: temurin
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
@@ -46,9 +40,9 @@ jobs:
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
# No `-PnoSpotless` here yet because the upstream cache layer matches the
|
||||
|
||||
@@ -10,21 +10,11 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
deploy-v2-on-push:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: deploy-v2-push-V2
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
@@ -35,12 +25,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Depot CLI
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Get commit hashes for frontend and backend
|
||||
@@ -105,23 +90,9 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Build and push frontend image (Depot)
|
||||
if: env.USE_DEPOT == 'true' && steps.check-frontend.outputs.exists == 'false'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.16.0
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
context: .
|
||||
file: ./docker/frontend/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-latest
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push frontend image (Docker fork fallback)
|
||||
if: env.USE_DEPOT != 'true' && steps.check-frontend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
- name: Build and push frontend image
|
||||
if: steps.check-frontend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/frontend/Dockerfile
|
||||
@@ -134,23 +105,9 @@ jobs:
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push backend image (Depot)
|
||||
if: env.USE_DEPOT == 'true' && steps.check-backend.outputs.exists == 'false'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.16.0
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
context: .
|
||||
file: ./docker/backend/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-latest
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push backend image (Docker fork fallback)
|
||||
if: env.USE_DEPOT != 'true' && steps.check-backend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
- name: Build and push backend image
|
||||
if: steps.check-backend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/backend/Dockerfile
|
||||
|
||||
@@ -11,28 +11,17 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: "false"
|
||||
depot_cores:
|
||||
description: "Depot runner vCPU count (used in runs-on). Override for benchmarking. Tuned to 4 because bench showed 16 was within noise of 4."
|
||||
required: false
|
||||
type: string
|
||||
default: "4"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
docker-compose-tests:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '4') }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
checks: write
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
@@ -50,7 +39,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
@@ -59,9 +48,9 @@ jobs:
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
# When the PR changes the base image, test.sh builds it locally
|
||||
|
||||
@@ -5,23 +5,13 @@ name: Playwright E2E (live backend)
|
||||
# server.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
depot_cores:
|
||||
description: "Depot runner vCPU count (used in runs-on). Override for benchmarking."
|
||||
required: false
|
||||
type: string
|
||||
default: "8"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
playwright-e2e-live:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
@@ -62,7 +52,17 @@ jobs:
|
||||
# .test-state/playwright/coverage-pw/ for the post-process step
|
||||
# to aggregate. Chromium-only - other engines silently skip.
|
||||
PW_COVERAGE: "1"
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results.json
|
||||
run: task e2e:live
|
||||
- name: Flag flaky tests
|
||||
# Runs regardless of the test outcome: a flaky test (passed on retry)
|
||||
# leaves the step green, so this is the only place it surfaces. Emits
|
||||
# ::warning:: annotations + a job summary; never fails the job.
|
||||
if: always()
|
||||
working-directory: frontend
|
||||
run: npx tsx editor/scripts/report-flaky-tests.mts "$PLAYWRIGHT_JSON_OUTPUT_FILE"
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results.json
|
||||
- name: Generate JaCoCo report from e2e:live .exec
|
||||
if: always()
|
||||
id: live-coverage
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
if: always() && steps.pw-frontend-coverage.outputs.summary == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-frontend-coverage-${{ github.run_id }}
|
||||
name: playwright-frontend-coverage
|
||||
path: |
|
||||
.test-state/playwright/coverage-pw-summary/
|
||||
.test-state/playwright/coverage-pw/
|
||||
|
||||
@@ -5,23 +5,13 @@ name: Playwright E2E (stubbed)
|
||||
# mocks API responses in the browser.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
depot_cores:
|
||||
description: "Depot runner vCPU count (used in runs-on). Override for benchmarking. Tuned to 8 to match the other playwright workflows; bench showed flat scaling above 8."
|
||||
required: false
|
||||
type: string
|
||||
default: "8"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
playwright-e2e:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
@@ -44,11 +34,22 @@ jobs:
|
||||
VITE_BUILD_FOR_PREVIEW: "1"
|
||||
run: task frontend:build
|
||||
- name: Run stubbed E2E tests (chromium)
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results.json
|
||||
run: task e2e:stubbed -- --workers=3
|
||||
- name: Flag flaky tests
|
||||
# Runs regardless of the test outcome: a flaky test (passed on retry)
|
||||
# leaves the step green, so this is the only place it surfaces. Emits
|
||||
# ::warning:: annotations + a job summary; never fails the job.
|
||||
if: always()
|
||||
working-directory: frontend
|
||||
run: npx tsx editor/scripts/report-flaky-tests.mts "$PLAYWRIGHT_JSON_OUTPUT_FILE"
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results.json
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-report-stubbed-${{ github.run_id }}
|
||||
path: frontend/editor/playwright-report/
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
@@ -19,13 +19,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
files-changed:
|
||||
name: detect what files changed
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
licenses-frontend: ${{ steps.changes.outputs.licenses-frontend }}
|
||||
@@ -48,8 +44,8 @@ jobs:
|
||||
generate-frontend-license-report:
|
||||
if: needs.files-changed.outputs.licenses-frontend == 'true'
|
||||
name: Generate Frontend License Report
|
||||
needs: [pick, files-changed]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
@@ -98,6 +94,13 @@ jobs:
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
- name: Generate frontend license report (Push only)
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
PR_IS_FORK: "false"
|
||||
run: task frontend:licenses:generate
|
||||
|
||||
- name: Generate frontend license report (internal PR)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
env:
|
||||
@@ -292,7 +295,10 @@ jobs:
|
||||
base: main
|
||||
title: "Update Frontend 3rd Party Licenses"
|
||||
body: ${{ env.PR_BODY }}
|
||||
labels: Licenses,github-actions,frontend
|
||||
labels: |
|
||||
Licenses
|
||||
github-actions
|
||||
Front End
|
||||
draft: false
|
||||
delete-branch: true
|
||||
sign-commits: true
|
||||
@@ -311,15 +317,13 @@ jobs:
|
||||
|
||||
generate-backend-license-report:
|
||||
if: needs.files-changed.outputs.licenses-backend == 'true'
|
||||
needs: [pick, files-changed]
|
||||
needs: files-changed
|
||||
name: Generate Backend License Report
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
repository-projects: write # Required for enabling automerge
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
@@ -347,12 +351,13 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
- name: Check licenses and generate report
|
||||
id: license-check
|
||||
run: task backend:licenses:generate || echo "LICENSE_CHECK_FAILED=true" >> $GITHUB_ENV
|
||||
@@ -512,7 +517,10 @@ jobs:
|
||||
base: main
|
||||
title: "Update Backend 3rd Party Licenses"
|
||||
body: ${{ env.PR_BODY }}
|
||||
labels: Licenses,github-actions,backend
|
||||
labels: |
|
||||
Licenses
|
||||
github-actions
|
||||
Back End
|
||||
delete-branch: true
|
||||
sign-commits: true
|
||||
|
||||
|
||||
@@ -11,12 +11,8 @@ permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
frontend-validation:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
|
||||
@@ -12,13 +12,14 @@ on:
|
||||
- "true"
|
||||
- "false"
|
||||
platform:
|
||||
description: "Platform to build (windows, macos, linux, or all)"
|
||||
description: "Platform to build (windows, windows-arm64, macos, linux, or all)"
|
||||
required: true
|
||||
default: "all"
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- windows
|
||||
- windows-arm64
|
||||
- macos
|
||||
- linux
|
||||
sign:
|
||||
@@ -36,13 +37,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
determine-matrix:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||
@@ -61,7 +58,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -71,9 +68,9 @@ jobs:
|
||||
gradle-${{ runner.os }}-
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
@@ -91,31 +88,40 @@ jobs:
|
||||
- name: Determine build matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
# windows-arm64: NSIS only (WiX MSI has no arm64 support in Tauri) and no
|
||||
# JPDFium natives yet - flip to windows-arm64 once JPDFium ships them.
|
||||
WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}'
|
||||
WINDOWS_ARM64='{"platform":"windows-11-arm","args":"--target aarch64-pc-windows-msvc --bundles nsis","name":"windows-arm64","jpdfium_platforms":"none"}'
|
||||
MACOS='{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"}'
|
||||
LINUX='{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}'
|
||||
ALL="$WINDOWS,$WINDOWS_ARM64,$MACOS,$LINUX"
|
||||
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
case "${{ github.event.inputs.platform }}" in
|
||||
"windows")
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}]}' >> $GITHUB_OUTPUT
|
||||
echo "matrix={\"include\":[$WINDOWS,$WINDOWS_ARM64]}" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"windows-arm64")
|
||||
echo "matrix={\"include\":[$WINDOWS_ARM64]}" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"macos")
|
||||
echo 'matrix={"include":[{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"}]}' >> $GITHUB_OUTPUT
|
||||
echo "matrix={\"include\":[$MACOS]}" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"linux")
|
||||
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}]}' >> $GITHUB_OUTPUT
|
||||
echo "matrix={\"include\":[$LINUX]}" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
*)
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"},{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}]}' >> $GITHUB_OUTPUT
|
||||
echo "matrix={\"include\":[$ALL]}" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# For push/release events, build all platforms
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"},{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}]}' >> $GITHUB_OUTPUT
|
||||
echo "matrix={\"include\":[$ALL]}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build-jars:
|
||||
needs: [pick, determine-matrix]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
needs: determine-matrix
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
variant:
|
||||
@@ -146,9 +152,9 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Setup Node.js
|
||||
if: matrix.variant.build_frontend == true
|
||||
@@ -195,7 +201,6 @@ jobs:
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
@@ -243,16 +248,17 @@ jobs:
|
||||
if: matrix.platform == 'macos-15'
|
||||
run: echo "X64_JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV"
|
||||
|
||||
# Temurin has no windows-aarch64 JDK 25 yet; Microsoft OpenJDK does.
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
@@ -278,7 +284,7 @@ jobs:
|
||||
# DigiCert KeyLocker Setup (Cloud HSM)
|
||||
- name: Setup DigiCert KeyLocker
|
||||
id: digicert-setup
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1
|
||||
env:
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
@@ -288,7 +294,7 @@ jobs:
|
||||
SM_HOST: ${{ secrets.SM_HOST }}
|
||||
|
||||
- name: Setup DigiCert KeyLocker Certificate
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Setting up DigiCert KeyLocker environment..."
|
||||
@@ -323,7 +329,7 @@ jobs:
|
||||
|
||||
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
|
||||
- name: Import Windows Code Signing Certificate
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY == '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
env:
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||
@@ -391,7 +397,7 @@ jobs:
|
||||
# Without this, signCommand failures are opaque (Tauri captures but drops
|
||||
# smctl's stderr) - running these loudly surfaces auth/env/keypair issues.
|
||||
- name: Preflight smctl
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
shell: pwsh
|
||||
env:
|
||||
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||
@@ -422,7 +428,7 @@ jobs:
|
||||
# smctl reads SM_HOST, SM_API_KEY, SM_CLIENT_CERT_FILE, SM_CLIENT_CERT_PASSWORD
|
||||
# from env (set by prior DigiCert setup step). No --config-file needed.
|
||||
- name: Configure Windows code signing
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
shell: bash
|
||||
env:
|
||||
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||
@@ -493,6 +499,54 @@ jobs:
|
||||
args: ${{ matrix.args }}
|
||||
updaterJsonKeepUniversal: true
|
||||
|
||||
# Bundled libwayland conflicts with the host's on some distros (Fedora
|
||||
# Wayland: EGL_BAD_PARAMETER, blank window - #6878). Repack without it,
|
||||
# then regenerate the updater .sig (repack invalidates the original) and
|
||||
# GPG-sign again when release signing is on.
|
||||
- name: Strip bundled Wayland libs from AppImage
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
continue-on-error: true
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
GPG_SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')) && '1' || '0' }}
|
||||
SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }}
|
||||
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
AI=$(find "$PWD/frontend/editor/src-tauri/target" -name "*.AppImage" | head -1)
|
||||
if [ -z "$AI" ]; then echo "No AppImage found - skipping"; exit 0; fi
|
||||
chmod +x "$AI"
|
||||
WORK=$(mktemp -d)
|
||||
(cd "$WORK" && "$AI" --appimage-extract >/dev/null)
|
||||
if ! ls "$WORK/squashfs-root/usr/lib/"libwayland-* >/dev/null 2>&1; then
|
||||
echo "No bundled libwayland - nothing to strip"
|
||||
rm -rf "$WORK"
|
||||
exit 0
|
||||
fi
|
||||
rm -f "$WORK/squashfs-root/usr/lib/"libwayland-*
|
||||
curl -fsSL -o "$WORK/appimagetool" \
|
||||
https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
# Pinned checksum: never execute an unverified downloaded binary. On
|
||||
# mismatch (upstream rebuilt continuous) the step aborts and the
|
||||
# original AppImage ships unchanged - update the pin deliberately.
|
||||
echo "a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0 $WORK/appimagetool" | sha256sum -c -
|
||||
chmod +x "$WORK/appimagetool"
|
||||
SIGN_ARGS=()
|
||||
if [ "$GPG_SIGN" = "1" ] && [ -n "${SIGN_KEY:-}" ]; then
|
||||
SIGN_ARGS=(--sign --sign-key "$SIGN_KEY")
|
||||
fi
|
||||
"$WORK/appimagetool" --appimage-extract-and-run "${SIGN_ARGS[@]}" "$WORK/squashfs-root" "$AI.new"
|
||||
# Updater payload signature must match the repacked bytes. The CLI
|
||||
# reads the key/password from env - never pass secrets as argv.
|
||||
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
||||
(cd frontend && npx tauri signer sign "$AI.new")
|
||||
mv "$AI.new.sig" "$AI.sig"
|
||||
fi
|
||||
mv "$AI.new" "$AI"
|
||||
rm -rf "$WORK"
|
||||
echo "Stripped bundled libwayland from $(basename "$AI")"
|
||||
|
||||
- name: Clear release GPG key from runner keyring (Linux)
|
||||
if: always() && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
|
||||
env:
|
||||
@@ -509,10 +563,31 @@ jobs:
|
||||
# artifact. Tauri signs a COPY when bundling into the MSI and leaves the raw
|
||||
# cargo output unsigned, so checking it produces false negatives.
|
||||
- name: Verify Windows Code Signature
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
timeout-minutes: 15
|
||||
shell: pwsh
|
||||
run: |
|
||||
# arm64 ships an NSIS installer, not an MSI. Tauri's signCommand signs the
|
||||
# inner exe before packing and the setup exe after, so verifying the setup
|
||||
# exe is the arm64 equivalent of the MSI + inner-exe check below.
|
||||
if ("${{ matrix.platform }}" -eq "windows-11-arm") {
|
||||
$setupExes = Get-ChildItem -Path "./frontend/editor/src-tauri/target" -Filter "*-setup.exe" -Recurse -File
|
||||
if ($setupExes.Count -eq 0) {
|
||||
Write-Host "[ERROR] No NSIS installer found under target/"
|
||||
exit 1
|
||||
}
|
||||
foreach ($exe in $setupExes) {
|
||||
$sig = Get-AuthenticodeSignature -FilePath $exe.FullName
|
||||
Write-Host "NSIS installer: $($exe.Name) Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
|
||||
if ($sig.Status -ne "Valid") {
|
||||
Write-Host "[ERROR] NSIS installer is not signed"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
Write-Host "[SUCCESS] NSIS installer is properly signed"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$allSigned = $true
|
||||
|
||||
# Check MSI installer (outer wrapper - what users download)
|
||||
@@ -576,7 +651,7 @@ jobs:
|
||||
# but drops stderr when the command exits non-zero, making failures opaque.
|
||||
# The real errors live in smctl's log files - surface them here for debugging.
|
||||
- name: Dump smctl logs on failure
|
||||
if: ${{ failure() && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }}
|
||||
if: ${{ failure() && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$logDir = "$env:USERPROFILE\.signingmanager\logs"
|
||||
@@ -612,6 +687,11 @@ jobs:
|
||||
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||
find . -name "*.msi" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.msi" \;
|
||||
find . -name "*.msi.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.msi.sig" \;
|
||||
elif [ "${{ matrix.platform }}" = "windows-11-arm" ]; then
|
||||
# arm64 ships the NSIS installer (WiX MSI has no arm64 support in Tauri).
|
||||
# The setup exe is also its own updater payload (-> sibling .sig).
|
||||
find . -name "*-setup.exe" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}-setup.exe" \;
|
||||
find . -name "*-setup.exe.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}-setup.exe.sig" \;
|
||||
elif [ "${{ matrix.platform }}" = "macos-15" ]; then
|
||||
# DMG = manual install; .app.tar.gz (+ .sig) = updater payload.
|
||||
# Raw .app is intentionally not shipped (hundreds of MB of uncompressed input).
|
||||
@@ -638,8 +718,8 @@ jobs:
|
||||
retention-days: 1
|
||||
|
||||
collect-and-release:
|
||||
needs: [pick, determine-matrix, build, build-jars]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
needs: [determine-matrix, build, build-jars]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
@@ -726,6 +806,10 @@ jobs:
|
||||
'bundles': ['Stirling-PDF-windows-x86_64.msi'],
|
||||
'targets': ['windows-x86_64-msi', 'windows-x86_64'],
|
||||
},
|
||||
{
|
||||
'bundles': ['Stirling-PDF-windows-arm64-setup.exe'],
|
||||
'targets': ['windows-aarch64-nsis', 'windows-aarch64'],
|
||||
},
|
||||
{
|
||||
'bundles': ['Stirling-PDF-macos-universal.app.tar.gz'],
|
||||
'targets': ['darwin-x86_64', 'darwin-aarch64'],
|
||||
@@ -824,6 +908,7 @@ jobs:
|
||||
files: |
|
||||
./artifacts/**/*.jar
|
||||
./artifacts/**/*.msi
|
||||
./artifacts/**/*-setup.exe
|
||||
./artifacts/**/*.dmg
|
||||
./artifacts/**/*.app.tar.gz
|
||||
./artifacts/**/*.deb
|
||||
|
||||
@@ -13,13 +13,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
playwright-all-browsers:
|
||||
name: Playwright (chromium + firefox + webkit)
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
@@ -53,8 +49,8 @@ jobs:
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-nightly-${{ github.run_id }}
|
||||
path: frontend/editor/playwright-report/
|
||||
name: playwright-report-nightly-${{ github.run_id }}
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 14
|
||||
|
||||
# Builds all desktop platforms on a schedule so the Rust dependency cache is
|
||||
|
||||
@@ -22,15 +22,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
push:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
@@ -46,9 +40,9 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Generate Swagger documentation
|
||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||
|
||||
@@ -12,7 +12,7 @@ on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
platform:
|
||||
description: "Platform to build (windows, macos, linux, or all)."
|
||||
description: "Platform to build (windows, windows-arm64, macos, linux, windows-macos, or all)."
|
||||
required: false
|
||||
type: string
|
||||
default: "all"
|
||||
@@ -21,23 +21,35 @@ on:
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
minimal:
|
||||
description: "Fast smoke build: Linux deb only, skip rpm and the flaky AppImage pass. Used by PR builds."
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
description: "Platform to build (windows, macos, linux, or all)"
|
||||
description: "Platform to build (windows, windows-arm64, macos, linux, windows-macos, or all)"
|
||||
required: true
|
||||
default: "all"
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- windows
|
||||
- windows-arm64
|
||||
- macos
|
||||
- linux
|
||||
- windows-macos
|
||||
sign:
|
||||
description: "Sign and notarize the bundles."
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
minimal:
|
||||
description: "Fast smoke build: Linux deb only, skip rpm and the flaky AppImage pass."
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -51,7 +63,7 @@ jobs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -62,14 +74,19 @@ jobs:
|
||||
PLATFORM: ${{ inputs.platform }}
|
||||
run: |
|
||||
WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}'
|
||||
# ARM64: NSIS only (WiX MSI has no arm64 support in Tauri) and no JPDFium
|
||||
# natives yet - flip jpdfium_platforms to windows-arm64 once JPDFium ships it.
|
||||
WINDOWS_ARM64='{"platform":"windows-11-arm","args":"--target aarch64-pc-windows-msvc --bundles nsis","name":"windows-arm64","jpdfium_platforms":"none"}'
|
||||
MACOS='{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"}'
|
||||
LINUX='{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}'
|
||||
|
||||
case "$PLATFORM" in
|
||||
windows) ENTRIES=("$WINDOWS") ;;
|
||||
macos) ENTRIES=("$MACOS") ;;
|
||||
linux) ENTRIES=("$LINUX") ;;
|
||||
*) ENTRIES=("$WINDOWS" "$MACOS" "$LINUX") ;;
|
||||
windows) ENTRIES=("$WINDOWS" "$WINDOWS_ARM64") ;;
|
||||
windows-arm64) ENTRIES=("$WINDOWS_ARM64") ;;
|
||||
macos) ENTRIES=("$MACOS") ;;
|
||||
linux) ENTRIES=("$LINUX") ;;
|
||||
windows-macos) ENTRIES=("$WINDOWS" "$MACOS") ;;
|
||||
*) ENTRIES=("$WINDOWS" "$WINDOWS_ARM64" "$MACOS" "$LINUX") ;;
|
||||
esac
|
||||
|
||||
# Drop macOS entries when Apple certificate secret is unavailable
|
||||
@@ -96,10 +113,14 @@ jobs:
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
# Per-platform sign gate. macOS signs on any run with the cert available,
|
||||
# PRs included: Gatekeeper blocks an unsigned .dmg, so an unsigned macOS
|
||||
# PR build is not testable. Windows and Linux stay main-only, matching the
|
||||
# gates on their own signing steps below.
|
||||
SIGN_BUNDLE: ${{ inputs.sign && (matrix.platform == 'macos-15' && secrets.APPLE_CERTIFICATE != '' || github.ref == 'refs/heads/main') }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -113,7 +134,7 @@ jobs:
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
@@ -151,16 +172,17 @@ jobs:
|
||||
if: matrix.platform == 'macos-15'
|
||||
run: echo "X64_JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV"
|
||||
|
||||
# Temurin has no windows-aarch64 JDK 25 yet; Microsoft OpenJDK does.
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Setup Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
@@ -187,7 +209,7 @@ jobs:
|
||||
# DigiCert KeyLocker Setup (Cloud HSM)
|
||||
- name: Setup DigiCert KeyLocker
|
||||
id: digicert-setup
|
||||
if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1
|
||||
env:
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
@@ -197,7 +219,7 @@ jobs:
|
||||
SM_HOST: ${{ secrets.SM_HOST }}
|
||||
|
||||
- name: Setup DigiCert KeyLocker Certificate
|
||||
if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Setting up DigiCert KeyLocker environment..."
|
||||
@@ -232,7 +254,7 @@ jobs:
|
||||
|
||||
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
|
||||
- name: Import Windows Code Signing Certificate
|
||||
if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }}
|
||||
env:
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||
@@ -263,7 +285,7 @@ jobs:
|
||||
}
|
||||
|
||||
- name: Import Apple Developer Certificate
|
||||
if: inputs.sign && matrix.platform == 'macos-15' && env.APPLE_CERTIFICATE != ''
|
||||
if: env.SIGN_BUNDLE == 'true' && matrix.platform == 'macos-15'
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
@@ -284,7 +306,7 @@ jobs:
|
||||
rm certificate.p12
|
||||
|
||||
- name: Verify Certificate
|
||||
if: inputs.sign && matrix.platform == 'macos-15' && env.APPLE_CERTIFICATE != ''
|
||||
if: env.SIGN_BUNDLE == 'true' && matrix.platform == 'macos-15'
|
||||
run: |
|
||||
echo "Verifying Apple Developer Certificate..."
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
@@ -307,7 +329,7 @@ jobs:
|
||||
ls -la /usr/bin/hd* || echo "No hd* tools found"
|
||||
|
||||
- name: Preflight smctl
|
||||
if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
shell: pwsh
|
||||
env:
|
||||
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||
@@ -320,7 +342,7 @@ jobs:
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "[WARN] smctl windows certsync returned non-zero - continuing" }
|
||||
|
||||
- name: Configure Windows code signing
|
||||
if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
shell: bash
|
||||
env:
|
||||
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||
@@ -357,7 +379,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Build Tauri app (signed)
|
||||
if: inputs.sign
|
||||
if: env.SIGN_BUNDLE == 'true'
|
||||
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -386,13 +408,13 @@ jobs:
|
||||
with:
|
||||
projectPath: ./frontend/editor
|
||||
tauriScript: npx tauri
|
||||
# Linux: build deb+rpm only here. AppImage runs in its own
|
||||
# continue-on-error step below so its persistent linuxdeploy
|
||||
# failure (#6127 onwards) does not tank deb/rpm uploads.
|
||||
args: ${{ matrix.platform == 'ubuntu-22.04' && '--bundles deb,rpm' || matrix.args }}
|
||||
# Linux: build deb+rpm only here (deb-only on minimal smoke builds).
|
||||
# AppImage runs in its own continue-on-error step below so its
|
||||
# persistent linuxdeploy failure (#6127 onwards) does not tank uploads.
|
||||
args: ${{ matrix.platform == 'ubuntu-22.04' && (inputs.minimal && '--bundles deb' || '--bundles deb,rpm') || matrix.args }}
|
||||
|
||||
- name: Build Tauri app (unsigned)
|
||||
if: ${{ !inputs.sign }}
|
||||
if: env.SIGN_BUNDLE != 'true'
|
||||
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -406,15 +428,18 @@ jobs:
|
||||
with:
|
||||
projectPath: ./frontend/editor
|
||||
tauriScript: npx tauri
|
||||
# Linux: build deb+rpm only here. AppImage runs in its own
|
||||
# continue-on-error step below so its persistent linuxdeploy
|
||||
# failure (#6127 onwards) does not tank deb/rpm uploads.
|
||||
args: ${{ matrix.platform == 'ubuntu-22.04' && '--bundles deb,rpm' || matrix.args }}
|
||||
# Linux: build deb+rpm only here (deb-only on minimal smoke builds).
|
||||
# AppImage runs in its own continue-on-error step below so its
|
||||
# persistent linuxdeploy failure (#6127 onwards) does not tank uploads.
|
||||
args: >-
|
||||
${{ matrix.platform == 'ubuntu-22.04' && (inputs.minimal && '--bundles deb' || '--bundles deb,rpm') || matrix.args }}
|
||||
--config '{"bundle":{"createUpdaterArtifacts":false}}'
|
||||
|
||||
# AppImage is decoupled so its linuxdeploy run gets a fresh process
|
||||
# (rpm scratch state torn down) and its failure can't tank deb/rpm.
|
||||
# Skipped on minimal smoke builds (flaky + slow, deb is enough to verify).
|
||||
- name: Build Tauri app (Linux AppImage)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
if: matrix.platform == 'ubuntu-22.04' && !inputs.minimal
|
||||
continue-on-error: true
|
||||
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
||||
env:
|
||||
@@ -433,6 +458,37 @@ jobs:
|
||||
tauriScript: npx tauri
|
||||
args: --bundles appimage
|
||||
|
||||
# Bundled libwayland conflicts with the host's on some distros (Fedora
|
||||
# Wayland: EGL_BAD_PARAMETER, blank window - #6878). The AppImage
|
||||
# ecosystem excludelist agrees these libs must come from the system.
|
||||
- name: Strip bundled Wayland libs from AppImage
|
||||
if: matrix.platform == 'ubuntu-22.04' && !inputs.minimal
|
||||
continue-on-error: true
|
||||
run: |
|
||||
set -euo pipefail
|
||||
AI=$(find "$PWD/frontend/editor/src-tauri/target" -name "*.AppImage" | head -1)
|
||||
if [ -z "$AI" ]; then echo "No AppImage found - skipping"; exit 0; fi
|
||||
chmod +x "$AI"
|
||||
WORK=$(mktemp -d)
|
||||
(cd "$WORK" && "$AI" --appimage-extract >/dev/null)
|
||||
if ! ls "$WORK/squashfs-root/usr/lib/"libwayland-* >/dev/null 2>&1; then
|
||||
echo "No bundled libwayland - nothing to strip"
|
||||
rm -rf "$WORK"
|
||||
exit 0
|
||||
fi
|
||||
rm -f "$WORK/squashfs-root/usr/lib/"libwayland-*
|
||||
curl -fsSL -o "$WORK/appimagetool" \
|
||||
https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
# Pinned checksum: never execute an unverified downloaded binary. On
|
||||
# mismatch (upstream rebuilt continuous) the step aborts and the
|
||||
# original AppImage ships unchanged - update the pin deliberately.
|
||||
echo "a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0 $WORK/appimagetool" | sha256sum -c -
|
||||
chmod +x "$WORK/appimagetool"
|
||||
"$WORK/appimagetool" --appimage-extract-and-run "$WORK/squashfs-root" "$AI.new"
|
||||
mv "$AI.new" "$AI"
|
||||
rm -rf "$WORK"
|
||||
echo "Stripped bundled libwayland from $(basename "$AI")"
|
||||
|
||||
- name: Clear release GPG key from runner keyring (Linux)
|
||||
if: always() && inputs.sign && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main'
|
||||
env:
|
||||
@@ -444,7 +500,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Verify notarization (macOS only)
|
||||
if: inputs.sign && matrix.platform == 'macos-15'
|
||||
if: env.SIGN_BUNDLE == 'true' && matrix.platform == 'macos-15'
|
||||
run: |
|
||||
echo "🔍 Verifying notarization status..."
|
||||
cd ./frontend/editor/src-tauri/target
|
||||
@@ -471,6 +527,9 @@ jobs:
|
||||
# Only ship the MSI installer. The loose exe and WiX toolset exes
|
||||
# are not the user-facing installer - the MSI contains the signed inner exe.
|
||||
find . -name "*.msi" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.msi" \;
|
||||
elif [ "${{ matrix.platform }}" = "windows-11-arm" ]; then
|
||||
# arm64 ships the NSIS installer (WiX MSI has no arm64 support in Tauri).
|
||||
find . -name "*-setup.exe" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}-setup.exe" \;
|
||||
elif [ "${{ matrix.platform }}" = "macos-15" ]; then
|
||||
find . -name "*.dmg" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
||||
else
|
||||
@@ -482,9 +541,28 @@ jobs:
|
||||
# Verify the MSI AND the inner exe extracted from it are signed.
|
||||
# The inner exe is what gets installed on users' machines and what AV scans.
|
||||
- name: Verify Windows Code Signature
|
||||
if: inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main'
|
||||
if: inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main'
|
||||
shell: pwsh
|
||||
run: |
|
||||
# arm64 ships an NSIS installer, not an MSI. Tauri's signCommand signs the
|
||||
# inner exe before packing and the setup exe after, so verifying the setup
|
||||
# exe is the arm64 equivalent of the MSI + inner-exe check below.
|
||||
if ("${{ matrix.platform }}" -eq "windows-11-arm") {
|
||||
$exePath = "./dist/Stirling-PDF-${{ matrix.name }}-setup.exe"
|
||||
if (-not (Test-Path $exePath)) {
|
||||
Write-Host "[ERROR] NSIS installer not found at $exePath"
|
||||
exit 1
|
||||
}
|
||||
$sig = Get-AuthenticodeSignature -FilePath $exePath
|
||||
Write-Host "NSIS installer: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
|
||||
if ($sig.Status -ne "Valid") {
|
||||
Write-Host "[ERROR] NSIS installer is not signed"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[SUCCESS] NSIS installer is properly signed"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$allSigned = $true
|
||||
$msiPath = "./dist/Stirling-PDF-${{ matrix.name }}.msi"
|
||||
|
||||
@@ -530,7 +608,7 @@ jobs:
|
||||
Write-Host "[SUCCESS] MSI and inner exe are properly signed"
|
||||
|
||||
- name: Dump smctl logs on failure
|
||||
if: ${{ failure() && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }}
|
||||
if: ${{ failure() && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$logDir = "$env:USERPROFILE\.signingmanager\logs"
|
||||
@@ -557,7 +635,7 @@ jobs:
|
||||
cd ./frontend/editor/src-tauri/target
|
||||
|
||||
# Check for expected artifacts based on platform
|
||||
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||
if [ "${{ matrix.platform }}" = "windows-latest" ] || [ "${{ matrix.platform }}" = "windows-11-arm" ]; then
|
||||
echo "Checking for Windows artifacts..."
|
||||
find . -name "*.exe" -o -name "*.msi" | head -5
|
||||
if [ $(find . -name "*.exe" | wc -l) -eq 0 ]; then
|
||||
@@ -601,12 +679,19 @@ jobs:
|
||||
pr-comment:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request' && needs.build.result == 'success'
|
||||
# Fork and Dependabot pull_request runs receive a read-only GITHUB_TOKEN,
|
||||
# so the API cannot create or update PR comments there. The artifacts are
|
||||
# still uploaded and remain available from the Actions run page.
|
||||
if: >-
|
||||
github.event_name == 'pull_request' &&
|
||||
needs.build.result == 'success' &&
|
||||
!github.event.pull_request.head.repo.fork &&
|
||||
github.actor != 'dependabot[bot]'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden the runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -629,6 +714,7 @@ jobs:
|
||||
// Map of expected artifact names to display info
|
||||
const artifactMap = {
|
||||
'Stirling-PDF-windows-x86_64': { icon: '🪟', platform: 'Windows x64', files: '.exe, .msi' },
|
||||
'Stirling-PDF-windows-arm64': { icon: '🪟', platform: 'Windows ARM64', files: '-setup.exe (NSIS)' },
|
||||
'Stirling-PDF-macos-universal': { icon: '🍎', platform: 'macOS Universal', files: '.dmg' },
|
||||
'Stirling-PDF-linux-x86_64': { icon: '🐧', platform: 'Linux x64', files: '.deb, .rpm, .AppImage' }
|
||||
};
|
||||
@@ -697,7 +783,7 @@ jobs:
|
||||
if: always()
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -12,19 +12,16 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: "false"
|
||||
depot_cores:
|
||||
description: "Depot runner vCPU count (used in runs-on). Override for benchmarking."
|
||||
dockerfiles-changed:
|
||||
description: "Whether any Dockerfile changed (forwarded from files-changed). Gates the slow arm64 build leg."
|
||||
required: false
|
||||
type: string
|
||||
default: "8"
|
||||
default: "false"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
# TODO: extract a pre-matrix `prepare` job that runs once and produces
|
||||
# shared artifacts for the three matrix entries below to consume:
|
||||
# 1. `task backend:build` — currently runs 3× in parallel with
|
||||
@@ -40,14 +37,7 @@ jobs:
|
||||
# spring-security=true matrix entry if `task backend:build` and
|
||||
# `task backend:build:ci` produce equivalent JARs (verify before wiring).
|
||||
test-build-docker-images:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' && inputs.docker-base-changed != 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -95,7 +85,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
@@ -104,9 +94,9 @@ jobs:
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Install Task
|
||||
@@ -120,16 +110,10 @@ jobs:
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
- name: Set up Depot CLI
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.0.0
|
||||
|
||||
- name: Set up QEMU
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: env.USE_DEPOT != 'true'
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
@@ -146,13 +130,22 @@ jobs:
|
||||
# GITHUB_EVENT_NAME is already provided by the runner.
|
||||
env:
|
||||
DOCKER_BASE_CHANGED: ${{ inputs.docker-base-changed }}
|
||||
DOCKERFILES_CHANGED: ${{ inputs.dockerfiles-changed }}
|
||||
run: |
|
||||
if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && [ "$DOCKER_BASE_CHANGED" = "true" ]; then
|
||||
# Base Dockerfile changed: build against the locally-built base,
|
||||
# which only exists for amd64.
|
||||
echo "base_image=stirling-pdf-base:pr-test" >> "$GITHUB_OUTPUT"
|
||||
echo "platforms=linux/amd64" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
elif [ "$DOCKERFILES_CHANGED" = "true" ]; then
|
||||
# A Dockerfile changed: also verify the arm64 build (slow QEMU leg).
|
||||
echo "base_image=stirlingtools/stirling-pdf-base:latest" >> "$GITHUB_OUTPUT"
|
||||
echo "platforms=linux/amd64,linux/arm64/v8" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# No Dockerfile change: amd64 only. arm64 is exercised on the base
|
||||
# image publish and on release, not on every code PR.
|
||||
echo "base_image=stirlingtools/stirling-pdf-base:latest" >> "$GITHUB_OUTPUT"
|
||||
echo "platforms=linux/amd64" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Base-changed PRs build the embedded image with the local docker driver
|
||||
@@ -168,25 +161,11 @@ jobs:
|
||||
--tag stirling-pdf-embedded:pr-test \
|
||||
.
|
||||
|
||||
- name: Build ${{ matrix.docker-rev }} (Depot)
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.16.0
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
context: .
|
||||
file: ./${{ matrix.docker-rev }}
|
||||
push: false
|
||||
platforms: ${{ steps.build-params.outputs.platforms }}
|
||||
build-args: |
|
||||
BASE_IMAGE=${{ steps.build-params.outputs.base_image }}
|
||||
provenance: true
|
||||
sbom: true
|
||||
|
||||
# Fork PRs that did NOT change the base use the buildx container builder
|
||||
# PRs that did NOT change the base use the buildx container builder
|
||||
# (multi-platform + gha cache) against the published base image.
|
||||
- name: Build ${{ matrix.docker-rev }} (Docker fork fallback)
|
||||
if: env.USE_DEPOT != 'true' && inputs.docker-base-changed != 'true'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
- name: Build ${{ matrix.docker-rev }}
|
||||
if: inputs.docker-base-changed != 'true'
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
@@ -213,14 +192,7 @@ jobs:
|
||||
if-no-files-found: warn
|
||||
|
||||
test-build-unoserver-image:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' && inputs.docker-base-changed != 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
@@ -230,36 +202,15 @@ jobs:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Depot CLI
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.0.0
|
||||
|
||||
- name: Set up QEMU
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: env.USE_DEPOT != 'true'
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Build docker/unoserver/Dockerfile (Depot)
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.16.0
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
context: .
|
||||
file: ./docker/unoserver/Dockerfile
|
||||
push: false
|
||||
load: true
|
||||
platforms: linux/amd64
|
||||
tags: stirling-unoserver:pr-test
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
- name: Build docker/unoserver/Dockerfile (Docker fork fallback)
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
- name: Build docker/unoserver/Dockerfile
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
|
||||
@@ -20,19 +20,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
deploy:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
@@ -49,9 +39,9 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
@@ -61,12 +51,7 @@ jobs:
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
|
||||
- name: Set up Depot CLI
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Get version number
|
||||
@@ -81,21 +66,8 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Build and push test image (Depot)
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.16.0
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
context: .
|
||||
file: ./docker/embedded/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:test-${{ github.sha }}
|
||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push test image (Docker fork fallback)
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
- name: Build and push test image
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/embedded/Dockerfile
|
||||
@@ -153,8 +125,7 @@ jobs:
|
||||
files-changed:
|
||||
if: always()
|
||||
name: detect what files changed
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
frontend: ${{ steps.changes.outputs.frontend }}
|
||||
@@ -174,8 +145,8 @@ jobs:
|
||||
|
||||
test:
|
||||
if: needs.files-changed.outputs.frontend == 'true'
|
||||
needs: [pick, deploy, files-changed]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
needs: [deploy, files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
@@ -208,8 +179,8 @@ jobs:
|
||||
FORCE_COLOR: "3"
|
||||
|
||||
cleanup:
|
||||
needs: [pick, deploy, test]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
needs: [deploy, test]
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
|
||||
@@ -22,6 +22,7 @@ vars:
|
||||
linux-amd64) echo "linux-x64";;
|
||||
linux-arm64) echo "linux-arm64";;
|
||||
windows-amd64) echo "windows-x64";;
|
||||
windows-arm64) echo "none";; # no JPDFium windows-arm64 natives published yet
|
||||
*) echo "all";;
|
||||
esac
|
||||
fi
|
||||
|
||||
+12
-6
@@ -62,18 +62,24 @@ dependencies {
|
||||
|
||||
api "com.stirling:jpdfium:${jpdfiumVersion}"
|
||||
|
||||
// -PjpdfiumPlatforms=all|<csv of linux-x64,linux-arm64,darwin-x64,darwin-arm64,windows-x64>
|
||||
// -PjpdfiumPlatforms=all|none|<csv of linux-x64,linux-arm64,darwin-x64,darwin-arm64,windows-x64>
|
||||
// 'none' skips natives entirely (windows-arm64 builds, until JPDFium ships that platform).
|
||||
def jpdfiumPlatformsProp = (project.findProperty('jpdfiumPlatforms') ?: 'all').toString().trim()
|
||||
def jpdfiumAllPlatforms = ['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64', 'windows-x64']
|
||||
def jpdfiumPlatforms = jpdfiumPlatformsProp == 'all'
|
||||
? jpdfiumAllPlatforms
|
||||
: jpdfiumPlatformsProp.split(',').collect { it.trim() }.findAll { it }
|
||||
def jpdfiumPlatforms
|
||||
if (jpdfiumPlatformsProp == 'all') {
|
||||
jpdfiumPlatforms = jpdfiumAllPlatforms
|
||||
} else if (jpdfiumPlatformsProp == 'none') {
|
||||
jpdfiumPlatforms = []
|
||||
} else {
|
||||
jpdfiumPlatforms = jpdfiumPlatformsProp.split(',').collect { it.trim() }.findAll { it }
|
||||
}
|
||||
def jpdfiumInvalid = jpdfiumPlatforms.findAll { !jpdfiumAllPlatforms.contains(it) }
|
||||
if (jpdfiumInvalid) {
|
||||
throw new GradleException("Unknown jpdfiumPlatforms value(s): ${jpdfiumInvalid.join(', ')}. " +
|
||||
"Valid: ${jpdfiumAllPlatforms.join(', ')} or 'all'.")
|
||||
"Valid: ${jpdfiumAllPlatforms.join(', ')}, 'all' or 'none'.")
|
||||
}
|
||||
logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms.join(', ')}")
|
||||
logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms ? jpdfiumPlatforms.join(', ') : 'none'}")
|
||||
jpdfiumPlatforms.each { platform ->
|
||||
runtimeOnly "com.stirling:jpdfium-natives-${platform}:${jpdfiumVersion}"
|
||||
}
|
||||
|
||||
+18
-1
@@ -144,8 +144,10 @@ public class TempFileCleanupService {
|
||||
int directoriesDeletedCount = 0;
|
||||
for (Path directory : registry.getTempDirectories()) {
|
||||
try {
|
||||
if (Files.exists(directory)) {
|
||||
if (Files.exists(directory)
|
||||
&& shouldDeleteRegisteredDirectory(directory, maxAgeMillis)) {
|
||||
GeneralUtils.deleteDirectory(directory);
|
||||
registry.unregisterDirectory(directory);
|
||||
directoriesDeletedCount++;
|
||||
log.debug("Cleaned up temporary directory: {}", directory);
|
||||
}
|
||||
@@ -275,6 +277,21 @@ public class TempFileCleanupService {
|
||||
return totalDeletedCount.get();
|
||||
}
|
||||
|
||||
private boolean shouldDeleteRegisteredDirectory(Path directory, long maxAgeMillis) {
|
||||
if (maxAgeMillis <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long lastModified = Files.getLastModifiedTime(directory).toMillis();
|
||||
return (currentTime - lastModified) > maxAgeMillis;
|
||||
} catch (IOException e) {
|
||||
log.debug("Could not check directory age, skipping cleanup: {}", directory, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the system temp directory path based on configuration or system property. */
|
||||
private Path getSystemTempPath() {
|
||||
String systemTempDir =
|
||||
|
||||
@@ -155,6 +155,7 @@ public class TempFileManager {
|
||||
if (directory != null && Files.isDirectory(directory)) {
|
||||
try {
|
||||
GeneralUtils.deleteDirectory(directory);
|
||||
registry.unregisterDirectory(directory);
|
||||
log.debug("Deleted temp directory: {}", directory.toString());
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to delete temp directory: {}", directory.toString(), e);
|
||||
|
||||
@@ -85,6 +85,18 @@ public class TempFileRegistry {
|
||||
return directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a temporary directory from the registry.
|
||||
*
|
||||
* @param directory The directory to unregister
|
||||
*/
|
||||
public void unregisterDirectory(Path directory) {
|
||||
if (directory != null) {
|
||||
tempDirectories.remove(directory);
|
||||
log.debug("Unregistered temp directory: {}", directory.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a third-party temporary file that requires special handling.
|
||||
*
|
||||
|
||||
+19
-1
@@ -176,11 +176,13 @@ class TempFileCleanupServiceMoreTest {
|
||||
class ScheduledCleanup {
|
||||
|
||||
@Test
|
||||
@DisplayName("deletes registered temp directories and reports counts")
|
||||
@DisplayName("deletes stale registered temp directories and reports counts")
|
||||
void deletesRegisteredDirectories() throws IOException {
|
||||
when(tempFileManager.cleanupOldTempFiles(anyLong())).thenReturn(2);
|
||||
Path regDir = Files.createDirectories(tempDir.resolve("registeredDir"));
|
||||
Files.createFile(regDir.resolve("inside.txt"));
|
||||
Files.setLastModifiedTime(
|
||||
regDir, FileTime.fromMillis(System.currentTimeMillis() - 2L * 60 * 60 * 1000));
|
||||
Set<Path> dirs = new HashSet<>();
|
||||
dirs.add(regDir);
|
||||
when(registry.getTempDirectories()).thenReturn(dirs);
|
||||
@@ -193,6 +195,22 @@ class TempFileCleanupServiceMoreTest {
|
||||
verify(tempFileManager).cleanupOldTempFiles(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("keeps a fresh registered temp directory")
|
||||
void keepsFreshRegisteredDirectory() throws IOException {
|
||||
when(tempFileManager.cleanupOldTempFiles(anyLong())).thenReturn(0);
|
||||
Path regDir = Files.createDirectories(tempDir.resolve("freshRegisteredDir"));
|
||||
Files.createFile(regDir.resolve("inside.txt"));
|
||||
Set<Path> dirs = new HashSet<>();
|
||||
dirs.add(regDir);
|
||||
when(registry.getTempDirectories()).thenReturn(dirs);
|
||||
lenient().when(registry.contains(any(File.class))).thenReturn(false);
|
||||
|
||||
withIsolatedUserHome(cleanupService::scheduledCleanup);
|
||||
|
||||
assertThat(Files.exists(regDir)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("skips a registered directory that no longer exists")
|
||||
void skipsMissingRegisteredDirectory() {
|
||||
|
||||
+20
@@ -57,6 +57,7 @@ public class PdfOverlayController {
|
||||
int overlayPos = request.getOverlayPosition();
|
||||
|
||||
MultipartFile[] overlayFiles = request.getOverlayFiles();
|
||||
validateOverlayFiles(overlayFiles);
|
||||
File[] overlayPdfFiles = new File[overlayFiles.length];
|
||||
List<File> tempFiles = new ArrayList<>(); // List to keep track of temporary files
|
||||
|
||||
@@ -116,10 +117,29 @@ public class PdfOverlayController {
|
||||
}
|
||||
}
|
||||
|
||||
// Both fields are declared required, but @ModelAttribute binding leaves them null when the
|
||||
// caller omits them, which would otherwise surface as a 500 instead of a 400.
|
||||
private void validateOverlayFiles(MultipartFile[] overlayFiles) {
|
||||
if (overlayFiles == null || overlayFiles.length == 0) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.overlayFilesRequired", "At least one overlay file is required");
|
||||
}
|
||||
for (MultipartFile overlayFile : overlayFiles) {
|
||||
if (overlayFile == null || overlayFile.isEmpty()) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.overlayFileEmpty", "Overlay files must not be empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Integer, String> prepareOverlayGuide(
|
||||
int basePageCount, File[] overlayFiles, String mode, int[] counts, List<File> tempFiles)
|
||||
throws IOException {
|
||||
Map<Integer, String> overlayGuide = new HashMap<>();
|
||||
if (mode == null) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidFormat", "Invalid {0} format: {1}", "overlay mode", "null");
|
||||
}
|
||||
switch (mode) {
|
||||
case "SequentialOverlay":
|
||||
sequentialOverlay(overlayGuide, overlayFiles, basePageCount, tempFiles);
|
||||
|
||||
+8
-2
@@ -136,11 +136,17 @@ public class RedactController {
|
||||
+ "Users can provide text patterns to redact, with options for regex and whole word matching. "
|
||||
+ "Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<Resource> redactPdf(@ModelAttribute RedactPdfRequest request) {
|
||||
String[] listOfText = request.getListOfText().split("\n");
|
||||
String rawListOfText = request.getListOfText();
|
||||
boolean useRegex = Boolean.TRUE.equals(request.getUseRegex());
|
||||
boolean wholeWordSearchBool = Boolean.TRUE.equals(request.getWholeWordSearch());
|
||||
|
||||
if (listOfText.length == 0 || (listOfText.length == 1 && listOfText[0].trim().isEmpty())) {
|
||||
if (rawListOfText == null || rawListOfText.trim().isEmpty()) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.redaction.no.patterns", "No text patterns provided for redaction");
|
||||
}
|
||||
|
||||
String[] listOfText = rawListOfText.split("\n");
|
||||
if (listOfText.length == 1 && listOfText[0].trim().isEmpty()) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.redaction.no.patterns", "No text patterns provided for redaction");
|
||||
}
|
||||
|
||||
+12
@@ -299,6 +299,18 @@ class RedactControllerMoreTest {
|
||||
verify(pdfDocumentFactory, never()).load(any(MultipartFile.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("null listOfText throws an illegal-argument error before any load")
|
||||
void nullPatternsThrows() throws Exception {
|
||||
RedactPdfRequest request = new RedactPdfRequest();
|
||||
request.setFileInput(pdfFile(new byte[] {1, 2, 3}));
|
||||
request.setListOfText(null);
|
||||
|
||||
assertThatThrownBy(() -> controller.redactPdf(request))
|
||||
.isInstanceOf(RuntimeException.class);
|
||||
verify(pdfDocumentFactory, never()).load(any(MultipartFile.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("null file input is reported as a failure")
|
||||
void nullFileThrows() {
|
||||
|
||||
+4
-2
@@ -843,8 +843,10 @@ public class AdminSettingsController {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for fields containing 'password' or 'secret'
|
||||
return lowerField.contains("password") || lowerField.contains("secret");
|
||||
// Check for fields containing 'password' or 'secret' or 'bottoken'
|
||||
return lowerField.contains("password")
|
||||
|| lowerField.contains("secret")
|
||||
|| lowerField.contains("bottoken");
|
||||
}
|
||||
|
||||
/** Create a masked representation for sensitive fields */
|
||||
|
||||
+3
-3
@@ -21,7 +21,7 @@ import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
|
||||
ext {
|
||||
springBootVersion = "4.0.6"
|
||||
pdfboxVersion = "3.0.7"
|
||||
pdfboxVersion = "3.0.8"
|
||||
imageioVersion = "3.13.1"
|
||||
lombokVersion = "1.18.46"
|
||||
bouncycastleVersion = "1.84"
|
||||
@@ -39,7 +39,7 @@ ext {
|
||||
bucket4jVersion = "8.19.0"
|
||||
archunitVersion = "1.4.2"
|
||||
batikVersion = "1.19"
|
||||
jpdfiumVersion = "1.0.2"
|
||||
jpdfiumVersion = "1.0.4"
|
||||
jwtVersion = "0.13.0"
|
||||
awsSdkVersion = "2.44.12"
|
||||
testcontainersMinioVersion = "1.21.4"
|
||||
@@ -91,7 +91,7 @@ springBoot {
|
||||
|
||||
allprojects {
|
||||
group = 'stirling.software'
|
||||
version = '2.14.2'
|
||||
version = '2.14.3'
|
||||
|
||||
configurations.configureEach {
|
||||
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
||||
|
||||
@@ -31,6 +31,8 @@ const OUTPUT_FILE = path.join(
|
||||
// package.json lives at the workspace root (frontend/), not editor/. The
|
||||
// script is at frontend/editor/scripts/, so walk up two levels.
|
||||
const PACKAGE_JSON = path.join(__dirname, "..", "..", "package.json");
|
||||
const PACKAGE_NAME_PATTERN =
|
||||
/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
||||
|
||||
// Ensure the output directory exists
|
||||
const outputDir = path.dirname(OUTPUT_FILE);
|
||||
@@ -81,9 +83,12 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const existingModuleUrls = loadExistingModuleUrls();
|
||||
|
||||
// Convert license-checker format to array
|
||||
const licenseArray = licenseData.map((dep) => {
|
||||
let licenseType = dep.licenseType;
|
||||
const projectUrl = getProjectUrl(dep.name, dep.link, existingModuleUrls);
|
||||
|
||||
// Handle missing or null licenses
|
||||
if (!licenseType || licenseType === null || licenseType === undefined) {
|
||||
@@ -121,9 +126,9 @@ try {
|
||||
dep.remoteVersion ||
|
||||
"unknown",
|
||||
licenseType: licenseType,
|
||||
repository: dep.link,
|
||||
url: dep.link,
|
||||
link: dep.link,
|
||||
repository: projectUrl,
|
||||
url: projectUrl,
|
||||
link: projectUrl,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -133,7 +138,7 @@ try {
|
||||
const licenseType = Array.isArray(dep.licenseType)
|
||||
? dep.licenseType.join(", ")
|
||||
: dep.licenseType || "Unknown";
|
||||
const licenseUrl = dep.link || getLicenseUrl(licenseType);
|
||||
const licenseUrl = getLicenseUrl(licenseType) || dep.link;
|
||||
|
||||
return {
|
||||
moduleName: dep.name,
|
||||
@@ -231,6 +236,9 @@ try {
|
||||
function getLicenseUrl(licenseType) {
|
||||
if (!licenseType || licenseType === "Unknown") return "";
|
||||
|
||||
const explicitLicenseUrl = licenseType.match(/https?:\/\/\S+/)?.[0];
|
||||
if (explicitLicenseUrl) return explicitLicenseUrl;
|
||||
|
||||
const licenseUrls = {
|
||||
MIT: "https://opensource.org/licenses/MIT",
|
||||
"MIT*": "https://opensource.org/licenses/MIT",
|
||||
@@ -285,6 +293,41 @@ function getLicenseUrl(licenseType) {
|
||||
return "";
|
||||
}
|
||||
|
||||
function getProjectUrl(packageName, reportedUrl, existingModuleUrls) {
|
||||
if (!PACKAGE_NAME_PATTERN.test(packageName)) {
|
||||
throw new Error(`Invalid package name: ${packageName}`);
|
||||
}
|
||||
|
||||
return (
|
||||
normalizeProjectUrl(reportedUrl) ||
|
||||
normalizeProjectUrl(existingModuleUrls.get(packageName)) ||
|
||||
`https://www.npmjs.com/package/${packageName}`
|
||||
);
|
||||
}
|
||||
|
||||
function loadExistingModuleUrls() {
|
||||
try {
|
||||
const existingReport = JSON.parse(readFileSync(OUTPUT_FILE, "utf8"));
|
||||
return new Map(
|
||||
(existingReport.dependencies ?? [])
|
||||
.filter((dependency) => dependency.moduleName && dependency.moduleUrl)
|
||||
.map((dependency) => [dependency.moduleName, dependency.moduleUrl]),
|
||||
);
|
||||
} catch {
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeProjectUrl(url) {
|
||||
if (!url || url === "n/a") return "";
|
||||
|
||||
return url
|
||||
.replace(/^git\+/, "")
|
||||
.replace(/^git:\/\/github\.com\//, "https://github.com/")
|
||||
.replace(/^github:/, "https://github.com/")
|
||||
.replace(/\.git$/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for potentially problematic licenses that may not be MIT/corporate compatible
|
||||
*/
|
||||
|
||||
@@ -71,9 +71,43 @@ fn parse_launch_files(args: &[String]) -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
// URLs the webview is allowed to show: the bundled app and dev server only.
|
||||
// Anything else (file://, https://...) must never replace the app UI.
|
||||
fn is_app_url(url: &tauri::Url) -> bool {
|
||||
match url.scheme() {
|
||||
"tauri" | "about" | "blob" | "data" => true,
|
||||
"http" | "https" => matches!(
|
||||
url.host_str(),
|
||||
Some("tauri.localhost") | Some("localhost") | Some("127.0.0.1")
|
||||
),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// WebKitGTK's DMA-BUF renderer crashes the web process on NVIDIA and some
|
||||
// Wayland stacks (blank window, app dying on tool switch). Opt out unless overridden.
|
||||
#[cfg(target_os = "linux")]
|
||||
if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() {
|
||||
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||
}
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(
|
||||
// Dropping a file outside a dropzone makes WebKit navigate the webview to
|
||||
// that file, killing the app UI and its close handler (window becomes
|
||||
// unclosable). Block every off-app navigation at the Rust layer.
|
||||
tauri::plugin::Builder::<tauri::Wry, ()>::new("navigation-guard")
|
||||
.on_navigation(|_webview, url| {
|
||||
let allowed = is_app_url(url);
|
||||
if !allowed {
|
||||
add_log(format!("🚫 Blocked webview navigation to: {}", url));
|
||||
}
|
||||
allowed
|
||||
})
|
||||
.build()
|
||||
)
|
||||
.plugin(
|
||||
tauri_plugin_log::Builder::new()
|
||||
.level(log::LevelFilter::Info)
|
||||
@@ -209,10 +243,20 @@ pub fn run() {
|
||||
// Use Tauri's built-in cleanup
|
||||
app_handle.cleanup_before_exit();
|
||||
}
|
||||
RunEvent::WindowEvent { event: WindowEvent::CloseRequested {.. }, .. } => {
|
||||
RunEvent::WindowEvent { event: WindowEvent::CloseRequested {.. }, label, .. } => {
|
||||
add_log("🔄 Window close requested (will cleanup on actual exit)...".to_string());
|
||||
// Don't cleanup here - let JavaScript handler prevent close if needed
|
||||
// Backend cleanup happens in ExitRequested when window actually closes
|
||||
//
|
||||
// Failsafe: if the webview somehow left the app (JS close handler gone,
|
||||
// window would stay open forever), destroy the window directly.
|
||||
if let Some(window) = app_handle.get_webview_window(&label) {
|
||||
let off_app = window.url().map(|u| !is_app_url(&u)).unwrap_or(false);
|
||||
if off_app {
|
||||
add_log(format!("🚨 Webview '{}' is off-app, destroying window directly", label));
|
||||
let _ = window.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
RunEvent::WindowEvent { event: WindowEvent::DragDrop(drag_drop_event), label, .. } => {
|
||||
use tauri::DragDropEvent;
|
||||
@@ -267,3 +311,39 @@ pub fn run() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::is_app_url;
|
||||
|
||||
fn allows(raw: &str) -> bool {
|
||||
is_app_url(&tauri::Url::parse(raw).expect("valid url"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_bundled_app_and_dev_server() {
|
||||
assert!(allows("tauri://localhost/index.html"));
|
||||
assert!(allows("http://tauri.localhost/"));
|
||||
assert!(allows("http://localhost:5173/"));
|
||||
assert!(allows("http://127.0.0.1:8080/api"));
|
||||
assert!(allows("about:blank"));
|
||||
assert!(allows("blob:http://localhost:5173/abc"));
|
||||
assert!(allows("data:text/html,hi"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocks_dropped_files() {
|
||||
// The #6872 lockup: webview navigating to a dropped PDF.
|
||||
assert!(!allows("file:///C:/Users/me/report.pdf"));
|
||||
assert!(!allows("file:///home/me/report.pdf"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocks_remote_origins() {
|
||||
assert!(!allows("https://example.com/"));
|
||||
assert!(!allows("http://evil.test/"));
|
||||
// Look-alike hosts must not slip past the allowlist.
|
||||
assert!(!allows("https://localhost.evil.test/"));
|
||||
assert!(!allows("https://nottauri.localhost.evil.test/"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "Stirling PDF",
|
||||
"mainBinaryName": "Stirling-PDF",
|
||||
"version": "2.14.2",
|
||||
"version": "2.14.3",
|
||||
"identifier": "stirling.pdf.dev",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
@@ -2,528 +2,528 @@
|
||||
"dependencies": [
|
||||
{
|
||||
"moduleName": "@atlaskit/pragmatic-drag-and-drop",
|
||||
"moduleUrl": "git+https://github.com/atlassian/pragmatic-drag-and-drop.git",
|
||||
"moduleUrl": "https://github.com/atlassian/pragmatic-drag-and-drop",
|
||||
"moduleVersion": "1.7.9",
|
||||
"moduleLicense": "Apache-2.0",
|
||||
"moduleLicenseUrl": "git+https://github.com/atlassian/pragmatic-drag-and-drop.git"
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "@cantoo/pdf-lib",
|
||||
"moduleUrl": "git+https://github.com/cantoo-scribe/pdf-lib.git",
|
||||
"moduleUrl": "https://github.com/cantoo-scribe/pdf-lib",
|
||||
"moduleVersion": "2.6.5",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/cantoo-scribe/pdf-lib.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@dnd-kit/core",
|
||||
"moduleUrl": "git+https://github.com/clauderic/dnd-kit.git",
|
||||
"moduleUrl": "https://github.com/clauderic/dnd-kit",
|
||||
"moduleVersion": "6.3.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/clauderic/dnd-kit.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/core",
|
||||
"moduleUrl": "https://registry.npmjs.org/@embedpdf/core/-/core-2.14.4.tgz",
|
||||
"moduleUrl": "https://www.npmjs.com/package/@embedpdf/core",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "https://registry.npmjs.org/@embedpdf/core/-/core-2.14.4.tgz"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/engines",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/models",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-annotation",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-attachment",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-bookmark",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-document-manager",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-export",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-history",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-interaction-manager",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-pan",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-print",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-redaction",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-render",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-rotate",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-scroll",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-search",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-selection",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-spread",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-thumbnail",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-tiling",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-viewport",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-zoom",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer",
|
||||
"moduleVersion": "2.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@emotion/react",
|
||||
"moduleUrl": "git+https://github.com/emotion-js/emotion.git#main",
|
||||
"moduleUrl": "https://github.com/emotion-js/emotion/tree/main/packages/react",
|
||||
"moduleVersion": "11.14.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/emotion-js/emotion.git#main"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@emotion/styled",
|
||||
"moduleUrl": "git+https://github.com/emotion-js/emotion.git#main",
|
||||
"moduleUrl": "https://github.com/emotion-js/emotion/tree/main/packages/styled",
|
||||
"moduleVersion": "11.14.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/emotion-js/emotion.git#main"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@iconify/react",
|
||||
"moduleUrl": "git+https://github.com/iconify/iconify.git",
|
||||
"moduleUrl": "https://github.com/iconify/iconify",
|
||||
"moduleVersion": "6.0.2",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/iconify/iconify.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@mantine/core",
|
||||
"moduleUrl": "git+https://github.com/mantinedev/mantine.git",
|
||||
"moduleUrl": "https://github.com/mantinedev/mantine",
|
||||
"moduleVersion": "8.3.18",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/mantinedev/mantine.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@mantine/dates",
|
||||
"moduleUrl": "git+https://github.com/mantinedev/mantine.git",
|
||||
"moduleUrl": "https://github.com/mantinedev/mantine",
|
||||
"moduleVersion": "8.3.18",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/mantinedev/mantine.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@mantine/dropzone",
|
||||
"moduleUrl": "git+https://github.com/mantinedev/mantine.git",
|
||||
"moduleUrl": "https://github.com/mantinedev/mantine",
|
||||
"moduleVersion": "8.3.18",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/mantinedev/mantine.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@mantine/hooks",
|
||||
"moduleUrl": "git+https://github.com/mantinedev/mantine.git",
|
||||
"moduleUrl": "https://github.com/mantinedev/mantine",
|
||||
"moduleVersion": "8.3.18",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/mantinedev/mantine.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@mui/icons-material",
|
||||
"moduleUrl": "git+https://github.com/mui/material-ui.git",
|
||||
"moduleUrl": "https://github.com/mui/material-ui",
|
||||
"moduleVersion": "9.0.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/mui/material-ui.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@mui/material",
|
||||
"moduleUrl": "git+https://github.com/mui/material-ui.git",
|
||||
"moduleUrl": "https://github.com/mui/material-ui",
|
||||
"moduleVersion": "9.0.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/mui/material-ui.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@posthog/react",
|
||||
"moduleUrl": "git+https://github.com/PostHog/posthog-js.git",
|
||||
"moduleUrl": "https://github.com/PostHog/posthog-js",
|
||||
"moduleVersion": "1.8.2",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/PostHog/posthog-js.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@reactour/tour",
|
||||
"moduleUrl": "git+https://github.com/elrumordelaluz/reactour.git",
|
||||
"moduleUrl": "https://github.com/elrumordelaluz/reactour",
|
||||
"moduleVersion": "3.8.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/elrumordelaluz/reactour.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@stripe/react-stripe-js",
|
||||
"moduleUrl": "https://github.com/stripe/react-stripe-js.git",
|
||||
"moduleUrl": "https://github.com/stripe/react-stripe-js",
|
||||
"moduleVersion": "4.0.2",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "https://github.com/stripe/react-stripe-js.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@stripe/stripe-js",
|
||||
"moduleUrl": "https://github.com/stripe/stripe-js.git",
|
||||
"moduleUrl": "https://github.com/stripe/stripe-js",
|
||||
"moduleVersion": "7.9.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "https://github.com/stripe/stripe-js.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@supabase/supabase-js",
|
||||
"moduleUrl": "https://github.com/supabase/supabase-js.git",
|
||||
"moduleUrl": "https://github.com/supabase/supabase-js",
|
||||
"moduleVersion": "2.100.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "https://github.com/supabase/supabase-js.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@tailwindcss/postcss",
|
||||
"moduleUrl": "https://github.com/tailwindlabs/tailwindcss.git",
|
||||
"moduleUrl": "https://github.com/tailwindlabs/tailwindcss",
|
||||
"moduleVersion": "4.2.2",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "https://github.com/tailwindlabs/tailwindcss.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@tanstack/react-virtual",
|
||||
"moduleUrl": "git+https://github.com/TanStack/virtual.git",
|
||||
"moduleUrl": "https://github.com/TanStack/virtual",
|
||||
"moduleVersion": "3.13.23",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/TanStack/virtual.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@tauri-apps/api",
|
||||
"moduleUrl": "git+https://github.com/tauri-apps/tauri.git",
|
||||
"moduleUrl": "https://github.com/tauri-apps/tauri",
|
||||
"moduleVersion": "2.10.1",
|
||||
"moduleLicense": "Apache-2.0 OR MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/tauri-apps/tauri.git"
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "@tauri-apps/plugin-dialog",
|
||||
"moduleUrl": "git+https://github.com/tauri-apps/plugins-workspace.git",
|
||||
"moduleUrl": "https://github.com/tauri-apps/plugins-workspace",
|
||||
"moduleVersion": "2.7.0",
|
||||
"moduleLicense": "MIT OR Apache-2.0",
|
||||
"moduleLicenseUrl": "git+https://github.com/tauri-apps/plugins-workspace.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@tauri-apps/plugin-fs",
|
||||
"moduleUrl": "git+https://github.com/tauri-apps/plugins-workspace.git",
|
||||
"moduleUrl": "https://github.com/tauri-apps/plugins-workspace",
|
||||
"moduleVersion": "2.5.0",
|
||||
"moduleLicense": "MIT OR Apache-2.0",
|
||||
"moduleLicenseUrl": "git+https://github.com/tauri-apps/plugins-workspace.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@tauri-apps/plugin-http",
|
||||
"moduleUrl": "git+https://github.com/tauri-apps/plugins-workspace.git",
|
||||
"moduleUrl": "https://github.com/tauri-apps/plugins-workspace",
|
||||
"moduleVersion": "2.5.7",
|
||||
"moduleLicense": "MIT OR Apache-2.0",
|
||||
"moduleLicenseUrl": "git+https://github.com/tauri-apps/plugins-workspace.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@tauri-apps/plugin-notification",
|
||||
"moduleUrl": "git+https://github.com/tauri-apps/plugins-workspace.git",
|
||||
"moduleUrl": "https://github.com/tauri-apps/plugins-workspace",
|
||||
"moduleVersion": "2.3.3",
|
||||
"moduleLicense": "MIT OR Apache-2.0",
|
||||
"moduleLicenseUrl": "git+https://github.com/tauri-apps/plugins-workspace.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@tauri-apps/plugin-shell",
|
||||
"moduleUrl": "git+https://github.com/tauri-apps/plugins-workspace.git",
|
||||
"moduleUrl": "https://github.com/tauri-apps/plugins-workspace",
|
||||
"moduleVersion": "2.3.5",
|
||||
"moduleLicense": "MIT OR Apache-2.0",
|
||||
"moduleLicenseUrl": "git+https://github.com/tauri-apps/plugins-workspace.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@userback/widget",
|
||||
"moduleUrl": "git+https://github.com/userback/widget-js.git",
|
||||
"moduleUrl": "https://github.com/userback/widget-js",
|
||||
"moduleVersion": "0.3.12",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/userback/widget-js.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "autoprefixer",
|
||||
"moduleUrl": "git+https://github.com/postcss/autoprefixer.git",
|
||||
"moduleUrl": "postcss/autoprefixer",
|
||||
"moduleVersion": "10.4.27",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/postcss/autoprefixer.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "axios",
|
||||
"moduleUrl": "git+https://github.com/axios/axios.git",
|
||||
"moduleUrl": "https://github.com/axios/axios",
|
||||
"moduleVersion": "1.15.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/axios/axios.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "d3",
|
||||
"moduleUrl": "git+https://github.com/d3/d3.git",
|
||||
"moduleUrl": "https://github.com/d3/d3",
|
||||
"moduleVersion": "7.9.0",
|
||||
"moduleLicense": "ISC",
|
||||
"moduleLicenseUrl": "git+https://github.com/d3/d3.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/ISC"
|
||||
},
|
||||
{
|
||||
"moduleName": "globals",
|
||||
"moduleUrl": "git+https://github.com/sindresorhus/globals.git",
|
||||
"moduleUrl": "sindresorhus/globals",
|
||||
"moduleVersion": "17.5.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/sindresorhus/globals.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "i18next",
|
||||
"moduleUrl": "git+https://github.com/i18next/i18next.git",
|
||||
"moduleUrl": "https://github.com/i18next/i18next",
|
||||
"moduleVersion": "25.10.10",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/i18next/i18next.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "i18next-browser-languagedetector",
|
||||
"moduleUrl": "git+https://github.com/i18next/i18next-browser-languageDetector.git",
|
||||
"moduleUrl": "https://github.com/i18next/i18next-browser-languageDetector",
|
||||
"moduleVersion": "8.2.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/i18next/i18next-browser-languageDetector.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "jszip",
|
||||
"moduleUrl": "git+https://github.com/Stuk/jszip.git",
|
||||
"moduleUrl": "https://github.com/Stuk/jszip",
|
||||
"moduleVersion": "3.10.1",
|
||||
"moduleLicense": "(MIT OR GPL-3.0-or-later)",
|
||||
"moduleLicenseUrl": "git+https://github.com/Stuk/jszip.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "license-report",
|
||||
"moduleUrl": "git+https://github.com/bepo65/license-report.git",
|
||||
"moduleUrl": "https://github.com/kessler/license-report",
|
||||
"moduleVersion": "6.8.2",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/bepo65/license-report.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "pdfjs-dist",
|
||||
"moduleUrl": "git+https://github.com/mozilla/pdf.js.git",
|
||||
"moduleUrl": "https://github.com/mozilla/pdf.js",
|
||||
"moduleVersion": "5.5.207",
|
||||
"moduleLicense": "Apache-2.0",
|
||||
"moduleLicenseUrl": "git+https://github.com/mozilla/pdf.js.git"
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "peerjs",
|
||||
"moduleUrl": "git+https://github.com/peers/peerjs.git",
|
||||
"moduleUrl": "https://github.com/peers/peerjs",
|
||||
"moduleVersion": "1.5.5",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/peers/peerjs.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "pixelmatch",
|
||||
"moduleUrl": "git+https://github.com/mapbox/pixelmatch.git",
|
||||
"moduleUrl": "https://github.com/mapbox/pixelmatch",
|
||||
"moduleVersion": "7.1.0",
|
||||
"moduleLicense": "ISC",
|
||||
"moduleLicenseUrl": "git+https://github.com/mapbox/pixelmatch.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/ISC"
|
||||
},
|
||||
{
|
||||
"moduleName": "posthog-js",
|
||||
"moduleUrl": "https://github.com/PostHog/posthog-js",
|
||||
"moduleVersion": "1.363.3",
|
||||
"moduleLicense": "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE",
|
||||
"moduleLicenseUrl": "https://github.com/PostHog/posthog-js"
|
||||
"moduleLicenseUrl": "https://github.com/PostHog/posthog-js/blob/main/LICENSE"
|
||||
},
|
||||
{
|
||||
"moduleName": "qrcode.react",
|
||||
"moduleUrl": "git+https://github.com/zpao/qrcode.react.git",
|
||||
"moduleUrl": "https://github.com/zpao/qrcode.react",
|
||||
"moduleVersion": "4.2.0",
|
||||
"moduleLicense": "ISC",
|
||||
"moduleLicenseUrl": "git+https://github.com/zpao/qrcode.react.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/ISC"
|
||||
},
|
||||
{
|
||||
"moduleName": "react",
|
||||
"moduleUrl": "git+https://github.com/facebook/react.git",
|
||||
"moduleUrl": "https://github.com/facebook/react",
|
||||
"moduleVersion": "19.2.4",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/facebook/react.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "react-dom",
|
||||
"moduleUrl": "git+https://github.com/facebook/react.git",
|
||||
"moduleUrl": "https://github.com/facebook/react",
|
||||
"moduleVersion": "19.2.4",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/facebook/react.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "react-easy-crop",
|
||||
"moduleUrl": "git+https://github.com/ValentinH/react-easy-crop.git",
|
||||
"moduleUrl": "https://github.com/ValentinH/react-easy-crop",
|
||||
"moduleVersion": "5.5.6",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/ValentinH/react-easy-crop.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "react-i18next",
|
||||
"moduleUrl": "git+https://github.com/i18next/react-i18next.git",
|
||||
"moduleUrl": "https://github.com/i18next/react-i18next",
|
||||
"moduleVersion": "16.6.6",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/i18next/react-i18next.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "react-markdown",
|
||||
"moduleUrl": "git+https://github.com/remarkjs/react-markdown.git",
|
||||
"moduleUrl": "remarkjs/react-markdown",
|
||||
"moduleVersion": "9.1.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/remarkjs/react-markdown.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "react-rnd",
|
||||
"moduleUrl": "git+https://github.com/bokuweb/react-rnd.git",
|
||||
"moduleUrl": "https://github.com/bokuweb/react-rnd",
|
||||
"moduleVersion": "10.5.3",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/bokuweb/react-rnd.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "react-router-dom",
|
||||
"moduleUrl": "git+https://github.com/remix-run/react-router.git",
|
||||
"moduleUrl": "https://github.com/remix-run/react-router",
|
||||
"moduleVersion": "7.13.2",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/remix-run/react-router.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "recharts",
|
||||
"moduleUrl": "git+https://github.com/recharts/recharts.git",
|
||||
"moduleUrl": "https://github.com/recharts/recharts",
|
||||
"moduleVersion": "3.8.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/recharts/recharts.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "remark-gfm",
|
||||
"moduleUrl": "git+https://github.com/remarkjs/remark-gfm.git",
|
||||
"moduleUrl": "remarkjs/remark-gfm",
|
||||
"moduleVersion": "4.0.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/remarkjs/remark-gfm.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "signature_pad",
|
||||
"moduleUrl": "git+https://github.com/szimek/signature_pad.git",
|
||||
"moduleUrl": "https://github.com/szimek/signature_pad",
|
||||
"moduleVersion": "5.1.3",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/szimek/signature_pad.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "smol-toml",
|
||||
"moduleUrl": "github:squirrelchat/smol-toml",
|
||||
"moduleUrl": "https://github.com/squirrelchat/smol-toml",
|
||||
"moduleVersion": "1.6.1",
|
||||
"moduleLicense": "BSD-3-Clause",
|
||||
"moduleLicenseUrl": "github:squirrelchat/smol-toml"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/BSD-3-Clause"
|
||||
},
|
||||
{
|
||||
"moduleName": "tailwindcss",
|
||||
"moduleUrl": "https://github.com/tailwindlabs/tailwindcss.git",
|
||||
"moduleUrl": "https://github.com/tailwindlabs/tailwindcss",
|
||||
"moduleVersion": "4.2.2",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "https://github.com/tailwindlabs/tailwindcss.git"
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "web-vitals",
|
||||
"moduleUrl": "git+https://github.com/GoogleChrome/web-vitals.git",
|
||||
"moduleUrl": "https://github.com/GoogleChrome/web-vitals",
|
||||
"moduleVersion": "5.1.0",
|
||||
"moduleLicense": "Apache-2.0",
|
||||
"moduleLicenseUrl": "git+https://github.com/GoogleChrome/web-vitals.git"
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -177,6 +177,7 @@ const AppConfigModalInner: React.FC<AppConfigModalProps> = ({
|
||||
runningEE,
|
||||
loginEnabled,
|
||||
handleCloseSync,
|
||||
config?.showSettingsWhenNoLogin ?? true,
|
||||
);
|
||||
|
||||
const activeLabel = useMemo(() => {
|
||||
|
||||
@@ -41,6 +41,7 @@ export const useConfigNavSections = (
|
||||
_runningEE: boolean = false,
|
||||
_loginEnabled: boolean = false,
|
||||
onRequestClose: () => void = () => {},
|
||||
_showSettingsWhenNoLogin: boolean = true,
|
||||
): ConfigNavSection[] => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
+5
-2
@@ -32,6 +32,9 @@ interface LicensesSectionBodyProps {
|
||||
dependencies: Dependency[];
|
||||
}
|
||||
|
||||
const getModuleUrl = (dependency: Dependency) =>
|
||||
dependency.moduleUrl || dependency.moduleLicenseUrl;
|
||||
|
||||
function LicensesSectionBody({
|
||||
title,
|
||||
description,
|
||||
@@ -106,9 +109,9 @@ function LicensesSectionBody({
|
||||
sortedDependencies.map((dependency) => (
|
||||
<Table.Tr key={getDependencyKey(dependency)}>
|
||||
<Table.Td>
|
||||
{dependency.moduleUrl ? (
|
||||
{getModuleUrl(dependency) ? (
|
||||
<Anchor
|
||||
href={dependency.moduleUrl}
|
||||
href={getModuleUrl(dependency)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@@ -34,13 +34,17 @@ export default function ToolSelector({
|
||||
const [shouldAutoFocus, setShouldAutoFocus] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Filter out excluded tools (like 'automate' itself) and tools that don't support automation
|
||||
// Filter out excluded tools (like 'automate' itself), tools that don't support
|
||||
// automation, and tools with no operationConfig - the executor resolves a step
|
||||
// through operationConfig, so offering one without it fails only at run time.
|
||||
const baseFilteredTools = useMemo(() => {
|
||||
return (
|
||||
Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][]
|
||||
).filter(
|
||||
([key, tool]) =>
|
||||
!excludeTools.includes(key) && getToolSupportsAutomate(tool),
|
||||
!excludeTools.includes(key) &&
|
||||
getToolSupportsAutomate(tool) &&
|
||||
Boolean(tool.operationConfig),
|
||||
);
|
||||
}, [toolRegistry, excludeTools]);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Stack,
|
||||
Text,
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import RestartAltIcon from "@mui/icons-material/RestartAlt";
|
||||
import { CropParametersHook } from "@app/hooks/tools/crop/useCropParameters";
|
||||
import { useAllFiles } from "@app/contexts/FileContext";
|
||||
import {
|
||||
useViewScopedFiles,
|
||||
useViewScopedFileStubs,
|
||||
} from "@app/hooks/tools/shared/useViewScopedFiles";
|
||||
import CropAreaSelector from "@app/components/tools/crop/CropAreaSelector";
|
||||
import CropCoordinateInputs from "@app/components/tools/crop/CropCoordinateInputs";
|
||||
import { DEFAULT_CROP_AREA } from "@app/constants/cropConstants";
|
||||
@@ -34,32 +37,20 @@ const CONTAINER_SIZE = 250; // Fit within actual pane width
|
||||
|
||||
const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { files, fileStubs } = useAllFiles();
|
||||
// Preview and measure the document the crop will actually apply to, so both
|
||||
// follow the viewer when the user switches files with the tool open.
|
||||
const [selectedStub = null] = useViewScopedFileStubs();
|
||||
const [selectedFile = null] = useViewScopedFiles();
|
||||
|
||||
// Get the first file for preview
|
||||
const selectedStub = useMemo(() => {
|
||||
return fileStubs.length > 0 ? fileStubs[0] : null;
|
||||
}, [fileStubs]);
|
||||
|
||||
// Get the first file for PDF processing
|
||||
const selectedFile = useMemo(() => {
|
||||
return files.length > 0 ? files[0] : null;
|
||||
}, [files]);
|
||||
|
||||
// Get thumbnail for the selected file
|
||||
const [thumbnail, setThumbnail] = useState<string | null>(null);
|
||||
const [pdfBounds, setPdfBounds] = useState<PDFBounds | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadPDFDimensions = async () => {
|
||||
if (!selectedStub || !selectedFile) {
|
||||
setPdfBounds(null);
|
||||
setThumbnail(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setThumbnail(selectedStub.thumbnailUrl || null);
|
||||
|
||||
try {
|
||||
// Get PDF dimensions from the actual file
|
||||
const arrayBuffer = await selectedFile.arrayBuffer();
|
||||
@@ -223,7 +214,7 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
|
||||
>
|
||||
<DocumentThumbnail
|
||||
file={selectedStub}
|
||||
thumbnail={thumbnail}
|
||||
thumbnail={selectedStub?.thumbnailUrl ?? null}
|
||||
style={{
|
||||
width: pdfBounds.thumbnailWidth,
|
||||
height: pdfBounds.thumbnailHeight,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
|
||||
const viewer = { activeFileIndex: 0 };
|
||||
const files = {
|
||||
files: [
|
||||
{ fileId: "a", name: "alpha.pdf" },
|
||||
{ fileId: "b", name: "beta.pdf" },
|
||||
],
|
||||
fileStubs: [
|
||||
{ id: "a", name: "alpha.pdf", thumbnailUrl: "alpha.png" },
|
||||
{ id: "b", name: "beta.pdf", thumbnailUrl: "beta.png" },
|
||||
],
|
||||
};
|
||||
|
||||
vi.mock("@app/contexts/ViewerContext", () => ({ useViewer: () => viewer }));
|
||||
vi.mock("@app/contexts/NavigationContext", () => ({
|
||||
useNavigationState: () => ({ workbench: "viewer" }),
|
||||
}));
|
||||
vi.mock("@app/contexts/FileContext", () => ({ useAllFiles: () => files }));
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (_key: string, fallback?: string) => fallback ?? _key,
|
||||
}),
|
||||
}));
|
||||
|
||||
import RotateSettings from "@app/components/tools/rotate/RotateSettings";
|
||||
|
||||
const parameters = {
|
||||
parameters: { angle: 0 },
|
||||
rotateClockwise: vi.fn(),
|
||||
rotateAnticlockwise: vi.fn(),
|
||||
} as never;
|
||||
|
||||
const renderAt = (activeFileIndex: number) => {
|
||||
viewer.activeFileIndex = activeFileIndex;
|
||||
return render(
|
||||
<MantineProvider>
|
||||
<RotateSettings parameters={parameters} />
|
||||
</MantineProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
describe("RotateSettings preview", () => {
|
||||
it("previews the viewer's active file, not the first loaded one", () => {
|
||||
renderAt(1);
|
||||
|
||||
expect(screen.getByAltText("Preview of beta.pdf")).toBeInTheDocument();
|
||||
expect(screen.queryByAltText("Preview of alpha.pdf")).toBeNull();
|
||||
});
|
||||
|
||||
it("follows the viewer when the active file changes", () => {
|
||||
const { rerender } = renderAt(0);
|
||||
expect(screen.getByAltText("Preview of alpha.pdf")).toBeInTheDocument();
|
||||
|
||||
viewer.activeFileIndex = 1;
|
||||
rerender(
|
||||
<MantineProvider>
|
||||
<RotateSettings parameters={parameters} />
|
||||
</MantineProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByAltText("Preview of beta.pdf")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { Stack, Text, Box, ActionIcon, Group, Center } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import RotateLeftIcon from "@mui/icons-material/RotateLeft";
|
||||
import RotateRightIcon from "@mui/icons-material/RotateRight";
|
||||
import { RotateParametersHook } from "@app/hooks/tools/rotate/useRotateParameters";
|
||||
import { useAllFiles } from "@app/contexts/FileContext";
|
||||
import { useViewScopedFileStubs } from "@app/hooks/tools/shared/useViewScopedFiles";
|
||||
import DocumentThumbnail from "@app/components/shared/filePreview/DocumentThumbnail";
|
||||
|
||||
interface RotateSettingsProps {
|
||||
@@ -17,19 +17,9 @@ const RotateSettings = ({
|
||||
disabled = false,
|
||||
}: RotateSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { fileStubs } = useAllFiles();
|
||||
|
||||
// Get the first file for preview
|
||||
const selectedStub = useMemo(() => {
|
||||
return fileStubs.length > 0 ? fileStubs[0] : null;
|
||||
}, [fileStubs]);
|
||||
|
||||
// Get thumbnail for the selected file
|
||||
const [thumbnail, setThumbnail] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setThumbnail(selectedStub?.thumbnailUrl || null);
|
||||
}, [selectedStub]);
|
||||
// Preview the document the rotation will actually apply to, so it follows
|
||||
// the viewer when the user switches files with the tool open.
|
||||
const [previewStub = null] = useViewScopedFileStubs();
|
||||
|
||||
// Calculate current angle display
|
||||
const currentAngle = parameters.parameters.angle;
|
||||
@@ -85,7 +75,10 @@ const RotateSettings = ({
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<DocumentThumbnail file={selectedStub} thumbnail={thumbnail} />
|
||||
<DocumentThumbnail
|
||||
file={previewStub}
|
||||
thumbnail={previewStub?.thumbnailUrl ?? null}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Center>
|
||||
|
||||
@@ -25,6 +25,12 @@ import ZoomInIcon from "@mui/icons-material/ZoomIn";
|
||||
import ZoomOutIcon from "@mui/icons-material/ZoomOut";
|
||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||
|
||||
// Sizing constants for the page number input
|
||||
const MIN_PAGE_DIGITS = 2;
|
||||
const MIN_INPUT_WIDTH_PX = 48;
|
||||
const BASE_INPUT_WIDTH_PX = 32;
|
||||
const PX_PER_DIGIT = 8;
|
||||
|
||||
interface PdfViewerToolbarProps {
|
||||
// Page navigation props (placeholders for now)
|
||||
currentPage?: number;
|
||||
@@ -138,6 +144,15 @@ export function PdfViewerToolbar({
|
||||
scrollActions.scrollToLastPage();
|
||||
};
|
||||
|
||||
const totalPagesDigits = Math.max(
|
||||
MIN_PAGE_DIGITS,
|
||||
(scrollState.totalPages || 1).toString().length,
|
||||
);
|
||||
const inputWidth = Math.max(
|
||||
MIN_INPUT_WIDTH_PX,
|
||||
BASE_INPUT_WIDTH_PX + totalPagesDigits * PX_PER_DIGIT,
|
||||
);
|
||||
|
||||
return (
|
||||
<Paper
|
||||
radius="xl xl 0 0"
|
||||
@@ -206,10 +221,13 @@ export function PdfViewerToolbar({
|
||||
hideControls
|
||||
styles={{
|
||||
input: {
|
||||
width: 48,
|
||||
width: inputWidth,
|
||||
textAlign: "center",
|
||||
fontWeight: 500,
|
||||
fontSize: 16,
|
||||
paddingLeft: 4,
|
||||
paddingRight: 4,
|
||||
boxSizing: "border-box",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -12,12 +12,15 @@ interface SearchInterfaceProps {
|
||||
export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
const { t } = useTranslation();
|
||||
const viewerContext = React.useContext(ViewerContext);
|
||||
const viewerContextRef = useRef(viewerContext);
|
||||
|
||||
useEffect(() => {
|
||||
viewerContextRef.current = viewerContext;
|
||||
}, [viewerContext]);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const searchState = viewerContext?.getSearchState();
|
||||
const searchResults = searchState?.results;
|
||||
const searchActiveIndex = searchState?.activeIndex;
|
||||
const searchActions = viewerContext?.searchActions;
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [jumpToValue, setJumpToValue] = useState("");
|
||||
@@ -88,7 +91,11 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
if (!visible) return;
|
||||
|
||||
const checkSearchState = () => {
|
||||
// Use ViewerContext state instead of window APIs
|
||||
// Fetch fresh search state from ViewerContext ref to avoid closure stale values
|
||||
const searchState = viewerContextRef.current?.getSearchState();
|
||||
const searchResults = searchState?.results;
|
||||
const searchActiveIndex = searchState?.activeIndex;
|
||||
|
||||
if (searchResults && searchResults.length > 0) {
|
||||
const activeIndex = searchActiveIndex || 1;
|
||||
|
||||
@@ -114,7 +121,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
const interval = setInterval(checkSearchState, 200);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [visible, searchResults, searchActiveIndex, searchQuery]);
|
||||
}, [visible, searchQuery]);
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
@@ -278,7 +285,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={handlePrevious}
|
||||
disabled={!resultInfo || resultInfo.currentIndex <= 1}
|
||||
disabled={!resultInfo || resultInfo.totalResults === 0}
|
||||
aria-label={t("viewer.search.previous", "Previous result")}
|
||||
>
|
||||
<LocalIcon icon="keyboard-arrow-up" width="1rem" height="1rem" />
|
||||
@@ -287,9 +294,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={handleNext}
|
||||
disabled={
|
||||
!resultInfo || resultInfo.currentIndex >= resultInfo.totalResults
|
||||
}
|
||||
disabled={!resultInfo || resultInfo.totalResults === 0}
|
||||
aria-label={t("viewer.search.next", "Next result")}
|
||||
>
|
||||
<LocalIcon icon="keyboard-arrow-down" width="1rem" height="1rem" />
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
ProcessedFileMetadata,
|
||||
} from "@app/types/fileContext";
|
||||
import { FileId, ToolOperation } from "@app/types/file";
|
||||
import { generateThumbnailWithMetadata } from "@app/utils/thumbnailUtils";
|
||||
import { generateThumbnailPairWithMetadata } from "@app/utils/thumbnailUtils";
|
||||
import { FileLifecycleManager } from "@app/contexts/file/lifecycle";
|
||||
import { buildQuickKeySet } from "@app/contexts/file/fileSelectors";
|
||||
import { StirlingFile } from "@app/types/fileContext";
|
||||
@@ -121,11 +121,20 @@ export async function generateProcessedFileMetadata(
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate unrotated thumbnails for PageEditor (rotation applied via CSS)
|
||||
const unrotatedResult = await generateThumbnailWithMetadata(file, false);
|
||||
// One parse produces both variants: unrotated thumbnails for PageEditor
|
||||
// (rotation applied via CSS) and the rotated one for file manager display.
|
||||
const { unrotated: unrotatedResult, rotated: rotatedResult } =
|
||||
await generateThumbnailPairWithMetadata(file);
|
||||
|
||||
// Generate rotated thumbnail for file manager display
|
||||
const rotatedResult = await generateThumbnailWithMetadata(file, true);
|
||||
// Large PDF whose linearized-prefix attempt failed: report "no metadata"
|
||||
// (the tolerated failure shape) rather than a bogus zero-page document.
|
||||
if (
|
||||
!unrotatedResult.thumbnail &&
|
||||
unrotatedResult.pageCount === 0 &&
|
||||
!unrotatedResult.isEncrypted
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const processedFile = createProcessedFile(
|
||||
unrotatedResult.pageCount,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Registry invariant: the Automate picker offers a tool whenever it doesn't opt out via
|
||||
* `supportsAutomate: false`, but automationExecutor resolves each step through the tool's
|
||||
* `operationConfig`. A tool that is offered without one is selectable in the builder and
|
||||
* only fails when the automation runs, with "Tool operation not supported: <toolId>".
|
||||
*
|
||||
* So a tool must either carry an operationConfig or declare supportsAutomate: false.
|
||||
*/
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { useTranslatedToolCatalog } from "@app/data/useTranslatedToolRegistry";
|
||||
import { getToolSupportsAutomate } from "@app/data/toolsTaxonomy";
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, fallback?: string) => fallback ?? key,
|
||||
i18n: { changeLanguage: vi.fn(), language: "en-US" },
|
||||
}),
|
||||
Trans: ({ children }: { children?: unknown }) => children,
|
||||
}));
|
||||
|
||||
describe("automatable tools", () => {
|
||||
test("every tool offered to Automate can be executed as a step", () => {
|
||||
const { result } = renderHook(() => useTranslatedToolCatalog());
|
||||
|
||||
const offeredWithoutConfig = Object.entries(result.current.regularTools)
|
||||
.filter(([, entry]) => entry && getToolSupportsAutomate(entry))
|
||||
.filter(([, entry]) => !entry.operationConfig)
|
||||
.map(([id]) => id);
|
||||
|
||||
expect(offeredWithoutConfig).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -49,6 +49,8 @@ import { changeMetadataOperationConfig } from "@app/hooks/tools/changeMetadata/u
|
||||
import { signOperationConfig } from "@app/hooks/tools/sign/useSignOperation";
|
||||
import { cropOperationConfig } from "@app/hooks/tools/crop/useCropOperation";
|
||||
import { removeAnnotationsOperationConfig } from "@app/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation";
|
||||
import { removeImageOperationConfig } from "@app/hooks/tools/removeImage/useRemoveImageOperation";
|
||||
import { pageLayoutOperationConfig } from "@app/hooks/tools/pageLayout/usePageLayoutOperation";
|
||||
import { extractImagesOperationConfig } from "@app/hooks/tools/extractImages/useExtractImagesOperation";
|
||||
import { replaceColorOperationConfig } from "@app/hooks/tools/replaceColor/useReplaceColorOperation";
|
||||
import { removePagesOperationConfig } from "@app/hooks/tools/removePages/useRemovePagesOperation";
|
||||
@@ -525,6 +527,9 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
|
||||
maxFiles: -1,
|
||||
endpoints: ["validate-signature"],
|
||||
synonyms: getSynonyms(t, "validateSignature"),
|
||||
// Reports on signatures rather than transforming the PDF, and its hook is
|
||||
// not on the operationConfig seam, so it cannot run as an automation step.
|
||||
supportsAutomate: false,
|
||||
automationSettings: null,
|
||||
},
|
||||
|
||||
@@ -729,6 +734,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
|
||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||
maxFiles: -1,
|
||||
endpoints: ["multi-page-layout"],
|
||||
operationConfig: asRegistryConfig(pageLayoutOperationConfig),
|
||||
automationSettings: lazySettings(
|
||||
() => import("@app/components/tools/pageLayout/PageLayoutSettings"),
|
||||
),
|
||||
@@ -941,7 +947,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
|
||||
subcategoryId: SubcategoryId.REMOVAL,
|
||||
maxFiles: -1,
|
||||
endpoints: ["remove-image-pdf"],
|
||||
operationConfig: undefined,
|
||||
operationConfig: asRegistryConfig(removeImageOperationConfig),
|
||||
synonyms: getSynonyms(t, "removeImage"),
|
||||
automationSettings: null,
|
||||
},
|
||||
@@ -1170,6 +1176,9 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
|
||||
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
||||
endpoints: ["scanner-effect"],
|
||||
synonyms: getSynonyms(t, "scannerEffect"),
|
||||
// No frontend implementation yet (component is null), so it has no
|
||||
// operationConfig to execute as an automation step.
|
||||
supportsAutomate: false,
|
||||
automationSettings: null,
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderHook } from "@testing-library/react";
|
||||
|
||||
const mockViewer = { activeFileIndex: 0 };
|
||||
const mockNavigation = { workbench: "viewer" };
|
||||
const mockFiles: { files: unknown[]; fileStubs: unknown[] } = {
|
||||
files: [],
|
||||
fileStubs: [],
|
||||
};
|
||||
|
||||
vi.mock("@app/contexts/ViewerContext", () => ({
|
||||
useViewer: () => mockViewer,
|
||||
}));
|
||||
|
||||
vi.mock("@app/contexts/NavigationContext", () => ({
|
||||
useNavigationState: () => mockNavigation,
|
||||
}));
|
||||
|
||||
vi.mock("@app/contexts/FileContext", () => ({
|
||||
useAllFiles: () => mockFiles,
|
||||
}));
|
||||
|
||||
import {
|
||||
useViewScopedFiles,
|
||||
useViewScopedFileStubs,
|
||||
} from "@app/hooks/tools/shared/useViewScopedFiles";
|
||||
|
||||
const file = (id: string) => ({ fileId: id, name: `${id}.pdf` });
|
||||
const stub = (id: string) => ({
|
||||
id,
|
||||
name: `${id}.pdf`,
|
||||
thumbnailUrl: `${id}.png`,
|
||||
});
|
||||
|
||||
function setState(opts: {
|
||||
workbench?: string;
|
||||
activeFileIndex?: number;
|
||||
files?: unknown[];
|
||||
fileStubs?: unknown[];
|
||||
}) {
|
||||
mockNavigation.workbench = opts.workbench ?? "viewer";
|
||||
mockViewer.activeFileIndex = opts.activeFileIndex ?? 0;
|
||||
mockFiles.files = opts.files ?? [];
|
||||
mockFiles.fileStubs = opts.fileStubs ?? [];
|
||||
}
|
||||
|
||||
describe("useViewScopedFileStubs", () => {
|
||||
it("follows the viewer's active file rather than the first loaded one", () => {
|
||||
setState({
|
||||
files: [file("a"), file("b"), file("c")],
|
||||
fileStubs: [stub("a"), stub("b"), stub("c")],
|
||||
activeFileIndex: 2,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useViewScopedFileStubs());
|
||||
|
||||
expect(result.current.map((s) => s.id)).toEqual(["c"]);
|
||||
});
|
||||
|
||||
it("re-resolves when the viewer switches files", () => {
|
||||
setState({
|
||||
files: [file("a"), file("b")],
|
||||
fileStubs: [stub("a"), stub("b")],
|
||||
activeFileIndex: 0,
|
||||
});
|
||||
|
||||
const { result, rerender } = renderHook(() => useViewScopedFileStubs());
|
||||
expect(result.current.map((s) => s.id)).toEqual(["a"]);
|
||||
|
||||
setState({
|
||||
files: [file("a"), file("b")],
|
||||
fileStubs: [stub("a"), stub("b")],
|
||||
activeFileIndex: 1,
|
||||
});
|
||||
rerender();
|
||||
|
||||
expect(result.current.map((s) => s.id)).toEqual(["b"]);
|
||||
});
|
||||
|
||||
it("matches by id, not position, when the two lists are out of step", () => {
|
||||
// A stub exists before its bytes load, so the stub list can be longer than
|
||||
// (and offset from) the file list.
|
||||
setState({
|
||||
files: [file("b")],
|
||||
fileStubs: [stub("a"), stub("b")],
|
||||
activeFileIndex: 0,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useViewScopedFileStubs());
|
||||
|
||||
expect(result.current.map((s) => s.id)).toEqual(["b"]);
|
||||
});
|
||||
|
||||
it("returns every stub outside the viewer, mirroring useViewScopedFiles", () => {
|
||||
setState({
|
||||
workbench: "fileEditor",
|
||||
files: [file("a"), file("b")],
|
||||
fileStubs: [stub("a"), stub("b")],
|
||||
});
|
||||
|
||||
const stubs = renderHook(() => useViewScopedFileStubs()).result.current;
|
||||
const files = renderHook(() => useViewScopedFiles()).result.current;
|
||||
|
||||
expect(stubs.map((s) => s.id)).toEqual(["a", "b"]);
|
||||
expect(stubs).toHaveLength(files.length);
|
||||
});
|
||||
|
||||
it("drops stubs with no loaded file instead of returning undefined holes", () => {
|
||||
setState({
|
||||
workbench: "fileEditor",
|
||||
files: [file("a")],
|
||||
fileStubs: [stub("a"), stub("orphan")],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useViewScopedFileStubs());
|
||||
|
||||
expect(result.current.map((s) => s.id)).toEqual(["a"]);
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { useMemo } from "react";
|
||||
import { useAllFiles } from "@app/contexts/FileContext";
|
||||
import { useViewer } from "@app/contexts/ViewerContext";
|
||||
import { useNavigationState } from "@app/contexts/NavigationContext";
|
||||
import { StirlingFile } from "@app/types/fileContext";
|
||||
import { StirlingFile, StirlingFileStub } from "@app/types/fileContext";
|
||||
|
||||
export function useViewScopedFiles(ignoreViewerScope = false): StirlingFile[] {
|
||||
const { activeFileIndex } = useViewer();
|
||||
@@ -18,3 +18,28 @@ export function useViewScopedFiles(ignoreViewerScope = false): StirlingFile[] {
|
||||
return allFiles;
|
||||
}, [workbench, allFiles, activeFileIndex, ignoreViewerScope]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The stubs for {@link useViewScopedFiles} — the same scoping rule, for UI that
|
||||
* needs stub-only metadata (thumbnails, page counts) rather than file bytes.
|
||||
* A tool's preview must read from here, not from the raw file list: indexing
|
||||
* that list pins the preview to whichever file was added first, so it keeps
|
||||
* showing the original document after the viewer switches files.
|
||||
*
|
||||
* Resolved by id rather than by position, because the file and stub lists are
|
||||
* filtered independently — a stub exists before its bytes finish loading, so
|
||||
* the two arrays can hold different indices for the same document.
|
||||
*/
|
||||
export function useViewScopedFileStubs(
|
||||
ignoreViewerScope = false,
|
||||
): StirlingFileStub[] {
|
||||
const scopedFiles = useViewScopedFiles(ignoreViewerScope);
|
||||
const { fileStubs } = useAllFiles();
|
||||
|
||||
return useMemo(() => {
|
||||
const stubsById = new Map(fileStubs.map((stub) => [stub.id, stub]));
|
||||
return scopedFiles
|
||||
.map((file) => stubsById.get(file.fileId))
|
||||
.filter((stub): stub is StirlingFileStub => stub != null);
|
||||
}, [scopedFiles, fileStubs]);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ import {
|
||||
const THUMBNAIL_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
||||
|
||||
export interface StoredStirlingFileRecord extends BaseFileMetadata {
|
||||
data: ArrayBuffer;
|
||||
// Blob since the large-file OOM fix (stored by reference, no JS-side copy);
|
||||
// ArrayBuffer records predate it and are still readable.
|
||||
data: ArrayBuffer | Blob;
|
||||
fileId: FileId; // Matches runtime StirlingFile.fileId exactly
|
||||
quickKey: string; // Matches runtime StirlingFile.quickKey exactly
|
||||
thumbnail?: string;
|
||||
@@ -114,7 +116,6 @@ class FileStorageService {
|
||||
stub: StirlingFileStub,
|
||||
): Promise<void> {
|
||||
const db = await this.getDatabase();
|
||||
const arrayBuffer = await stirlingFile.arrayBuffer();
|
||||
|
||||
const record: StoredStirlingFileRecord = {
|
||||
id: stirlingFile.fileId,
|
||||
@@ -125,7 +126,9 @@ class FileStorageService {
|
||||
size: stirlingFile.size,
|
||||
lastModified: stirlingFile.lastModified,
|
||||
createdAt: stub.createdAt,
|
||||
data: arrayBuffer,
|
||||
// Store the File (a Blob) itself: IndexedDB persists it by reference and
|
||||
// streams to disk, so multi-GB files never materialize in JS memory.
|
||||
data: stirlingFile,
|
||||
thumbnail: stub.thumbnailUrl,
|
||||
thumbnailStoredAt: stub.thumbnailUrl ? Date.now() : undefined,
|
||||
isLeaf: stub.isLeaf ?? true,
|
||||
|
||||
@@ -38,7 +38,7 @@ const FREE_LICENSE_INFO: LicenseInfo = {
|
||||
|
||||
const BASE_NO_LOGIN_CONFIG: AppConfig = {
|
||||
enableAnalytics: true,
|
||||
appVersion: "2.14.2",
|
||||
appVersion: "2.14.3",
|
||||
serverCertificateEnabled: false,
|
||||
enableAlphaFunctionality: false,
|
||||
serverPort: 8080,
|
||||
|
||||
@@ -82,6 +82,82 @@ export interface DecodedImage {
|
||||
height: number;
|
||||
}
|
||||
|
||||
function setImageObjectMatrix(
|
||||
m: WrappedPdfiumModule,
|
||||
imageObjPtr: number,
|
||||
pdfX: number,
|
||||
pdfY: number,
|
||||
drawWidth: number,
|
||||
drawHeight: number,
|
||||
): boolean {
|
||||
const matrixPtr = m.pdfium.wasmExports.malloc(6 * 4);
|
||||
try {
|
||||
m.pdfium.setValue(matrixPtr, drawWidth, "float");
|
||||
m.pdfium.setValue(matrixPtr + 4, 0, "float");
|
||||
m.pdfium.setValue(matrixPtr + 8, 0, "float");
|
||||
m.pdfium.setValue(matrixPtr + 12, drawHeight, "float");
|
||||
m.pdfium.setValue(matrixPtr + 16, pdfX, "float");
|
||||
m.pdfium.setValue(matrixPtr + 20, pdfY, "float");
|
||||
return m.FPDFPageObj_SetMatrix(imageObjPtr, matrixPtr);
|
||||
} finally {
|
||||
m.pdfium.wasmExports.free(matrixPtr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a PDFium image page object from decoded pixels.
|
||||
*
|
||||
* The caller owns the returned object until it is inserted into a page or
|
||||
* appended to an annotation. Destroy it with FPDFPageObj_Destroy on failure.
|
||||
*/
|
||||
export function createBitmapImageObject(
|
||||
m: WrappedPdfiumModule,
|
||||
docPtr: number,
|
||||
pagePtr: number,
|
||||
image: DecodedImage,
|
||||
pdfX: number,
|
||||
pdfY: number,
|
||||
drawWidth: number,
|
||||
drawHeight: number,
|
||||
): number | null {
|
||||
const bitmapPtr = m.FPDFBitmap_Create(image.width, image.height, 1);
|
||||
if (!bitmapPtr) return null;
|
||||
|
||||
try {
|
||||
const bufferPtr = m.FPDFBitmap_GetBuffer(bitmapPtr);
|
||||
const stride = m.FPDFBitmap_GetStride(bitmapPtr);
|
||||
|
||||
copyRgbaToBgraHeap(
|
||||
m,
|
||||
image.rgba,
|
||||
bufferPtr,
|
||||
image.width,
|
||||
image.height,
|
||||
stride,
|
||||
);
|
||||
|
||||
const imageObjPtr = m.FPDFPageObj_NewImageObj(docPtr);
|
||||
if (!imageObjPtr) return null;
|
||||
|
||||
if (!m.FPDFImageObj_SetBitmap(pagePtr, 0, imageObjPtr, bitmapPtr)) {
|
||||
m.FPDFPageObj_Destroy(imageObjPtr);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
!setImageObjectMatrix(m, imageObjPtr, pdfX, pdfY, drawWidth, drawHeight)
|
||||
) {
|
||||
m.FPDFPageObj_Destroy(imageObjPtr);
|
||||
return null;
|
||||
}
|
||||
|
||||
return imageObjPtr;
|
||||
} finally {
|
||||
// FPDFImageObj_SetBitmap copies the bitmap data into the image object.
|
||||
m.FPDFBitmap_Destroy(bitmapPtr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a PDFium bitmap from decoded RGBA pixels, attach it to a new image
|
||||
* page object, position it via an affine matrix, and insert it into the page.
|
||||
@@ -99,70 +175,20 @@ export function embedBitmapImageOnPage(
|
||||
drawWidth: number,
|
||||
drawHeight: number,
|
||||
): boolean {
|
||||
const bitmapPtr = m.FPDFBitmap_Create(image.width, image.height, 1);
|
||||
if (!bitmapPtr) return false;
|
||||
const imageObjPtr = createBitmapImageObject(
|
||||
m,
|
||||
docPtr,
|
||||
pagePtr,
|
||||
image,
|
||||
pdfX,
|
||||
pdfY,
|
||||
drawWidth,
|
||||
drawHeight,
|
||||
);
|
||||
if (!imageObjPtr) return false;
|
||||
|
||||
try {
|
||||
const bufferPtr = m.FPDFBitmap_GetBuffer(bitmapPtr);
|
||||
const stride = m.FPDFBitmap_GetStride(bitmapPtr);
|
||||
|
||||
copyRgbaToBgraHeap(
|
||||
m,
|
||||
image.rgba,
|
||||
bufferPtr,
|
||||
image.width,
|
||||
image.height,
|
||||
stride,
|
||||
);
|
||||
|
||||
const imageObjPtr = m.FPDFPageObj_NewImageObj(docPtr);
|
||||
if (!imageObjPtr) return false;
|
||||
|
||||
const setBitmapOk = m.FPDFImageObj_SetBitmap(
|
||||
pagePtr,
|
||||
0,
|
||||
imageObjPtr,
|
||||
bitmapPtr,
|
||||
);
|
||||
if (!setBitmapOk) {
|
||||
m.FPDFPageObj_Destroy(imageObjPtr);
|
||||
return false;
|
||||
}
|
||||
|
||||
// -- early-destroy the bitmap; PDFium has copied the pixel data internally
|
||||
m.FPDFBitmap_Destroy(bitmapPtr);
|
||||
|
||||
// Set affine transform: [a b c d e f]
|
||||
const matrixPtr = m.pdfium.wasmExports.malloc(6 * 4);
|
||||
try {
|
||||
m.pdfium.setValue(matrixPtr, drawWidth, "float"); // a — scaleX
|
||||
m.pdfium.setValue(matrixPtr + 4, 0, "float"); // b
|
||||
m.pdfium.setValue(matrixPtr + 8, 0, "float"); // c
|
||||
m.pdfium.setValue(matrixPtr + 12, drawHeight, "float"); // d — scaleY
|
||||
m.pdfium.setValue(matrixPtr + 16, pdfX, "float"); // e — translateX
|
||||
m.pdfium.setValue(matrixPtr + 20, pdfY, "float"); // f — translateY
|
||||
|
||||
if (!m.FPDFPageObj_SetMatrix(imageObjPtr, matrixPtr)) {
|
||||
m.FPDFPageObj_Destroy(imageObjPtr);
|
||||
return false;
|
||||
}
|
||||
} finally {
|
||||
m.pdfium.wasmExports.free(matrixPtr);
|
||||
}
|
||||
|
||||
m.FPDFPage_InsertObject(pagePtr, imageObjPtr);
|
||||
return true;
|
||||
} finally {
|
||||
// Safety net: FPDFBitmap_Destroy is a no-op if ptr is 0 in most PDFium
|
||||
// builds but guard anyway. If already destroyed above, the second call
|
||||
// is harmless because we allow it to be idempotent.
|
||||
// We use a try-catch to be safe across PDFium WASM builds.
|
||||
try {
|
||||
m.FPDFBitmap_Destroy(bitmapPtr);
|
||||
} catch {
|
||||
/* already freed */
|
||||
}
|
||||
}
|
||||
m.FPDFPage_InsertObject(pagePtr, imageObjPtr);
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Draw a simple light-grey rectangle as a placeholder for annotations
|
||||
@@ -206,8 +232,8 @@ export function decodeImageDataUrl(
|
||||
img.onload = () => {
|
||||
try {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
canvas.width = img.naturalWidth || img.width;
|
||||
canvas.height = img.naturalHeight || img.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
resolve(null);
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { loadScript, isScriptLoaded } from "@app/utils/scriptLoader";
|
||||
|
||||
/** jsdom doesn't fetch external scripts, so we drive the load/error events ourselves. */
|
||||
function scriptEl(id: string): HTMLScriptElement {
|
||||
const el = document.getElementById(id) as HTMLScriptElement | null;
|
||||
if (!el) throw new Error(`no script tag #${id}`);
|
||||
return el;
|
||||
}
|
||||
|
||||
describe("loadScript", () => {
|
||||
it("overlapping loads share one tag and resolve only after the script actually loads", async () => {
|
||||
const id = "test-script-overlap";
|
||||
const src = "https://example.test/overlap.js";
|
||||
|
||||
let firstResolved = false;
|
||||
let secondResolved = false;
|
||||
const p1 = loadScript({ src, id }).then(() => {
|
||||
firstResolved = true;
|
||||
});
|
||||
// Second, overlapping call (e.g. a StrictMode double-effect or a remount before the
|
||||
// first load settled). It must reuse the in-flight load, not resolve on tag existence.
|
||||
const p2 = loadScript({ src, id }).then(() => {
|
||||
secondResolved = true;
|
||||
});
|
||||
|
||||
// Only one tag despite two calls.
|
||||
expect(document.querySelectorAll(`#${id}`)).toHaveLength(1);
|
||||
|
||||
// Regression guard: before the script executes, neither promise resolves — previously
|
||||
// the second call resolved immediately because the tag existed, so callers ran before
|
||||
// the script's globals were defined (the "blank until you reopen" bug).
|
||||
await Promise.resolve();
|
||||
expect(firstResolved).toBe(false);
|
||||
expect(secondResolved).toBe(false);
|
||||
expect(isScriptLoaded(id)).toBe(false);
|
||||
|
||||
scriptEl(id).dispatchEvent(new Event("load"));
|
||||
await Promise.all([p1, p2]);
|
||||
expect(firstResolved).toBe(true);
|
||||
expect(secondResolved).toBe(true);
|
||||
expect(isScriptLoaded(id)).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves immediately once the script has already loaded", async () => {
|
||||
const id = "test-script-cached";
|
||||
const src = "https://example.test/cached.js";
|
||||
|
||||
const first = loadScript({ src, id });
|
||||
scriptEl(id).dispatchEvent(new Event("load"));
|
||||
await first;
|
||||
|
||||
// A later call must resolve from cache without waiting for a fresh load event (which
|
||||
// would never come) and without adding a second tag.
|
||||
await loadScript({ src, id });
|
||||
expect(document.querySelectorAll(`#${id}`)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("rejects when the script fails to load", async () => {
|
||||
const id = "test-script-error";
|
||||
const src = "https://example.test/error.js";
|
||||
|
||||
const p = loadScript({ src, id });
|
||||
scriptEl(id).dispatchEvent(new Event("error"));
|
||||
await expect(p).rejects.toThrow(/Failed to load script/);
|
||||
});
|
||||
|
||||
it("drops the failed tag so a retry re-attempts instead of hanging", async () => {
|
||||
const id = "test-script-retry";
|
||||
const src = "https://example.test/retry.js";
|
||||
|
||||
// First attempt (e.g. a warm-up) fails.
|
||||
const first = loadScript({ src, id });
|
||||
scriptEl(id).dispatchEvent(new Event("error"));
|
||||
await expect(first).rejects.toThrow(/Failed to load script/);
|
||||
// The poisoned tag must be gone — otherwise a retry would attach to a dead tag.
|
||||
expect(document.querySelectorAll(`#${id}`)).toHaveLength(0);
|
||||
|
||||
// Retry (e.g. the modal opening) creates a fresh tag and can now succeed.
|
||||
const second = loadScript({ src, id });
|
||||
expect(document.querySelectorAll(`#${id}`)).toHaveLength(1);
|
||||
scriptEl(id).dispatchEvent(new Event("load"));
|
||||
await expect(second).resolves.toBeUndefined();
|
||||
expect(isScriptLoaded(id)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,11 @@ interface ScriptLoadOptions {
|
||||
}
|
||||
|
||||
const loadedScripts = new Set<string>();
|
||||
// Loads that have started but not yet finished, keyed by script id/src. Overlapping
|
||||
// callers (e.g. a React StrictMode double-effect, or a component that remounts before
|
||||
// the previous load settled) reuse the same promise so they all resolve only when the
|
||||
// script has actually executed — never on tag existence alone.
|
||||
const pendingScripts = new Map<string, Promise<void>>();
|
||||
|
||||
export function loadScript({
|
||||
src,
|
||||
@@ -19,43 +24,75 @@ export function loadScript({
|
||||
defer = false,
|
||||
onLoad,
|
||||
}: ScriptLoadOptions): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Check if already loaded
|
||||
const scriptId = id || src;
|
||||
if (loadedScripts.has(scriptId)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const scriptId = id || src;
|
||||
|
||||
// Check if script already exists in DOM
|
||||
const existingScript = id
|
||||
? document.getElementById(id)
|
||||
: document.querySelector(`script[src="${src}"]`);
|
||||
if (existingScript) {
|
||||
// Already fully loaded and executed.
|
||||
if (loadedScripts.has(scriptId)) {
|
||||
onLoad?.();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// A load for the same script is already in flight — wait for it rather than kicking
|
||||
// off a second one (and, critically, don't resolve just because the tag is present).
|
||||
const inFlight = pendingScripts.get(scriptId);
|
||||
if (inFlight) {
|
||||
return onLoad ? inFlight.then(() => onLoad()) : inFlight;
|
||||
}
|
||||
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
const settleLoaded = (script: HTMLScriptElement) => {
|
||||
script.dataset.loaded = "true";
|
||||
loadedScripts.add(scriptId);
|
||||
pendingScripts.delete(scriptId);
|
||||
onLoad?.();
|
||||
resolve();
|
||||
};
|
||||
const settleError = (el: HTMLScriptElement) => {
|
||||
pendingScripts.delete(scriptId);
|
||||
// Drop the failed tag so a later retry (e.g. the modal after a warm-up that was
|
||||
// blocked by an extension) creates a fresh one and re-attempts, rather than
|
||||
// attaching to a dead tag whose error event won't fire again and hanging forever.
|
||||
el.remove();
|
||||
reject(new Error(`Failed to load script: ${src}`));
|
||||
};
|
||||
|
||||
// A matching tag may already be in the DOM (added by an earlier load whose Set/Map
|
||||
// state was lost, or injected elsewhere). If our own loader finished it, the
|
||||
// data-loaded flag is set and we can resolve immediately; otherwise attach to its
|
||||
// load lifecycle instead of assuming it is ready.
|
||||
const existing = (
|
||||
id
|
||||
? document.getElementById(id)
|
||||
: document.querySelector(`script[src="${src}"]`)
|
||||
) as HTMLScriptElement | null;
|
||||
if (existing) {
|
||||
if (existing.dataset.loaded === "true") {
|
||||
loadedScripts.add(scriptId);
|
||||
onLoad?.();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
existing.addEventListener("load", () => settleLoaded(existing), {
|
||||
once: true,
|
||||
});
|
||||
existing.addEventListener("error", () => settleError(existing), {
|
||||
once: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and append script
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
if (id) script.id = id;
|
||||
script.async = async;
|
||||
script.defer = defer;
|
||||
|
||||
script.onload = () => {
|
||||
loadedScripts.add(scriptId);
|
||||
if (onLoad) onLoad();
|
||||
resolve();
|
||||
};
|
||||
|
||||
script.onerror = () => {
|
||||
reject(new Error(`Failed to load script: ${src}`));
|
||||
};
|
||||
|
||||
script.addEventListener("load", () => settleLoaded(script), { once: true });
|
||||
script.addEventListener("error", () => settleError(script), { once: true });
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
pendingScripts.set(scriptId, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
export function isScriptLoaded(idOrSrc: string): boolean {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
PDFArray,
|
||||
PDFDict,
|
||||
PDFDocument,
|
||||
PDFName,
|
||||
PDFNumber,
|
||||
PDFRawStream,
|
||||
decodePDFRawStream,
|
||||
} from "@cantoo/pdf-lib";
|
||||
import { embedSignatureImages } from "@app/utils/signatureFlattening";
|
||||
|
||||
const ONE_PIXEL_PNG =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
|
||||
|
||||
beforeAll(async () => {
|
||||
const wasmPath = path.resolve(
|
||||
process.cwd(),
|
||||
"node_modules/@embedpdf/pdfium/dist/pdfium.wasm",
|
||||
);
|
||||
const wasmBytes = await readFile(wasmPath);
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () =>
|
||||
Promise.resolve(
|
||||
new Response(wasmBytes, {
|
||||
headers: { "Content-Type": "application/wasm" },
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
const readPageContentStreams = (document: PDFDocument): string[] => {
|
||||
const contents = document.getPage(0).node.Contents();
|
||||
if (!(contents instanceof PDFArray)) return [];
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
const streams: string[] = [];
|
||||
for (let index = 0; index < contents.size(); index++) {
|
||||
const stream = contents.lookup(index, PDFRawStream);
|
||||
streams.push(decoder.decode(decodePDFRawStream(stream).decode()));
|
||||
}
|
||||
return streams;
|
||||
};
|
||||
|
||||
describe("signatureFlattening", () => {
|
||||
test("adds a PDFium stamp without regenerating page content", async () => {
|
||||
const sourceDocument = await PDFDocument.create();
|
||||
const sourcePage = sourceDocument.addPage([300, 400]);
|
||||
const markerStream = sourceDocument.context.stream(
|
||||
"q\n% ORIGINAL_TYPE3_CONTENT\nQ\n",
|
||||
);
|
||||
sourcePage.node.addContentStream(
|
||||
sourceDocument.context.register(markerStream),
|
||||
);
|
||||
const sourceBytes = await sourceDocument.save();
|
||||
|
||||
const outputBytes = await embedSignatureImages(
|
||||
Uint8Array.from(sourceBytes).buffer,
|
||||
[
|
||||
{
|
||||
pageIndex: 0,
|
||||
annotations: [
|
||||
{
|
||||
id: "signature-1",
|
||||
// EmbedPDF may expose an internal asset reference here after the
|
||||
// annotation has been placed. The persisted PNG must win.
|
||||
imageData: "embedpdf-asset-reference",
|
||||
rect: {
|
||||
origin: { x: 25, y: 30 },
|
||||
size: { width: 120, height: 50 },
|
||||
},
|
||||
imageSrc: `data:image/png;base64,${ONE_PIXEL_PNG}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
(id) =>
|
||||
id === "signature-1"
|
||||
? `data:image/png;base64,${ONE_PIXEL_PNG}`
|
||||
: undefined,
|
||||
async () => ({
|
||||
width: 1,
|
||||
height: 1,
|
||||
rgba: new Uint8Array([0, 80, 180, 255]),
|
||||
}),
|
||||
);
|
||||
|
||||
const outputDocument = await PDFDocument.load(outputBytes);
|
||||
const contentStreams = readPageContentStreams(outputDocument);
|
||||
const annotations = outputDocument.getPage(0).node.Annots();
|
||||
|
||||
expect(contentStreams).toContain("q\n% ORIGINAL_TYPE3_CONTENT\nQ\n");
|
||||
expect(annotations).toBeInstanceOf(PDFArray);
|
||||
|
||||
const stamp = annotations?.lookup(0, PDFDict);
|
||||
const stampRect = stamp?.lookup(PDFName.of("Rect"), PDFArray);
|
||||
expect(stamp?.get(PDFName.of("Subtype"))).toEqual(PDFName.of("Stamp"));
|
||||
expect(stamp?.lookup(PDFName.of("F"), PDFNumber).asNumber()).toBe(196);
|
||||
expect(stamp?.get(PDFName.of("AP"))).toBeDefined();
|
||||
expect(
|
||||
Array.from({ length: stampRect?.size() ?? 0 }, (_, index) =>
|
||||
stampRect?.lookup(index, PDFNumber).asNumber(),
|
||||
),
|
||||
).toEqual([25, 320, 145, 370]);
|
||||
}, 20_000);
|
||||
});
|
||||
@@ -1,11 +1,15 @@
|
||||
// PDFium annotation subtype constants
|
||||
import {
|
||||
FPDF_ANNOT_INK,
|
||||
FPDF_ANNOT_LINE,
|
||||
embedBitmapImageOnPage,
|
||||
drawPlaceholderRect,
|
||||
createBitmapImageObject,
|
||||
decodeImageDataUrl,
|
||||
type DecodedImage,
|
||||
} from "@app/utils/pdfiumBitmapUtils";
|
||||
import {
|
||||
closeDocAndFreeBuffer,
|
||||
getPdfiumModule,
|
||||
openRawDocumentSafe,
|
||||
readEffectivePageBox,
|
||||
saveRawDocument,
|
||||
} from "@app/services/pdfiumService";
|
||||
import { generateThumbnailWithMetadata } from "@app/utils/thumbnailUtils";
|
||||
import {
|
||||
createChildStub,
|
||||
@@ -18,12 +22,6 @@ import {
|
||||
StirlingFileStub,
|
||||
} from "@app/types/fileContext";
|
||||
import type { SignatureAPI } from "@app/components/viewer/viewerTypes";
|
||||
import {
|
||||
getPdfiumModule,
|
||||
openRawDocumentSafe,
|
||||
closeDocAndFreeBuffer,
|
||||
saveRawDocument,
|
||||
} from "@app/services/pdfiumService";
|
||||
|
||||
interface MinimalFileContextSelectors {
|
||||
getAllFileIds: () => FileId[];
|
||||
@@ -75,23 +73,9 @@ export async function flattenSignatures(
|
||||
const pageAnnotations =
|
||||
await signatureApiRef.current.getPageAnnotations(pageIndex);
|
||||
if (pageAnnotations && pageAnnotations.length > 0) {
|
||||
const sessionAnnotations = pageAnnotations.filter((annotation) => {
|
||||
const hasStoredImageData =
|
||||
annotation.id && getImageData(annotation.id);
|
||||
const hasDirectImageData =
|
||||
annotation.imageData ||
|
||||
annotation.appearance ||
|
||||
annotation.stampData ||
|
||||
annotation.imageSrc ||
|
||||
annotation.contents ||
|
||||
annotation.data;
|
||||
return (
|
||||
hasStoredImageData ||
|
||||
(hasDirectImageData &&
|
||||
typeof hasDirectImageData === "string" &&
|
||||
hasDirectImageData.startsWith("data:image"))
|
||||
);
|
||||
});
|
||||
const sessionAnnotations = pageAnnotations.filter((annotation) =>
|
||||
Boolean(getAnnotationImageData(annotation, getImageData)),
|
||||
);
|
||||
|
||||
if (sessionAnnotations.length > 0) {
|
||||
allAnnotations.push({
|
||||
@@ -166,143 +150,23 @@ export async function flattenSignatures(
|
||||
type: "application/pdf",
|
||||
});
|
||||
|
||||
// Step 4: Manually render extracted annotations onto the PDF using PDFium WASM
|
||||
// Step 4: Add signatures as locked, printable PDFium stamp annotations.
|
||||
// FPDFAnnot_AppendObject creates the annotation appearance without asking
|
||||
// PDFium to regenerate the page's existing content. GenerateContent would
|
||||
// corrupt some Type3/vector content, including the issue #7083 logo.
|
||||
if (allAnnotations.length > 0) {
|
||||
try {
|
||||
const pdfArrayBufferForFlattening = await signedFile.arrayBuffer();
|
||||
const m = await getPdfiumModule();
|
||||
const docPtr = await openRawDocumentSafe(pdfArrayBufferForFlattening);
|
||||
|
||||
try {
|
||||
const pageCount = m.FPDF_GetPageCount(docPtr);
|
||||
|
||||
for (const pageData of allAnnotations) {
|
||||
const { pageIndex, annotations } = pageData;
|
||||
|
||||
if (pageIndex < pageCount) {
|
||||
const pagePtr = m.FPDF_LoadPage(docPtr, pageIndex);
|
||||
if (!pagePtr) continue;
|
||||
|
||||
const pageHeight = m.FPDF_GetPageHeightF(pagePtr);
|
||||
|
||||
for (const annotation of annotations) {
|
||||
try {
|
||||
const rect =
|
||||
annotation.rect ||
|
||||
annotation.bounds ||
|
||||
annotation.rectangle ||
|
||||
annotation.position;
|
||||
|
||||
if (rect) {
|
||||
const originalX =
|
||||
rect.origin?.x || rect.x || rect.left || 0;
|
||||
const originalY =
|
||||
rect.origin?.y || rect.y || rect.top || 0;
|
||||
const width = rect.size?.width || rect.width || 100;
|
||||
const height = rect.size?.height || rect.height || 50;
|
||||
|
||||
// Convert from CSS top-left to PDF bottom-left
|
||||
const pdfX = originalX;
|
||||
const pdfY = pageHeight - originalY - height;
|
||||
|
||||
let imageDataUrl =
|
||||
annotation.imageData ||
|
||||
annotation.appearance ||
|
||||
annotation.stampData ||
|
||||
annotation.imageSrc ||
|
||||
annotation.contents ||
|
||||
annotation.data;
|
||||
|
||||
if (!imageDataUrl && annotation.id) {
|
||||
const storedImageData = getImageData(annotation.id);
|
||||
if (storedImageData) {
|
||||
imageDataUrl = storedImageData;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert SVG to PNG first if needed
|
||||
if (
|
||||
imageDataUrl &&
|
||||
typeof imageDataUrl === "string" &&
|
||||
imageDataUrl.startsWith("data:image/svg+xml")
|
||||
) {
|
||||
const pngBytes = await rasteriseSvgToPng(
|
||||
imageDataUrl,
|
||||
width * 2,
|
||||
height * 2,
|
||||
);
|
||||
if (pngBytes) {
|
||||
imageDataUrl = await uint8ArrayToPngDataUrl(pngBytes);
|
||||
} else {
|
||||
drawPlaceholderRect(
|
||||
m,
|
||||
pagePtr,
|
||||
pdfX,
|
||||
pdfY,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
imageDataUrl &&
|
||||
typeof imageDataUrl === "string" &&
|
||||
imageDataUrl.startsWith("data:image")
|
||||
) {
|
||||
// Decode the image data URL to raw pixels via canvas
|
||||
const imageResult =
|
||||
await decodeImageDataUrl(imageDataUrl);
|
||||
if (imageResult) {
|
||||
embedBitmapImageOnPage(
|
||||
m,
|
||||
docPtr,
|
||||
pagePtr,
|
||||
imageResult,
|
||||
pdfX,
|
||||
pdfY,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
annotation.type === FPDF_ANNOT_INK ||
|
||||
annotation.type === FPDF_ANNOT_LINE
|
||||
) {
|
||||
drawPlaceholderRect(
|
||||
m,
|
||||
pagePtr,
|
||||
pdfX,
|
||||
pdfY,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (annotationError) {
|
||||
console.warn(
|
||||
"Failed to render annotation:",
|
||||
annotationError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
m.FPDFPage_GenerateContent(pagePtr);
|
||||
m.FPDF_ClosePage(pagePtr);
|
||||
}
|
||||
}
|
||||
|
||||
const resultBuf = await saveRawDocument(docPtr);
|
||||
signedFile = new File([resultBuf], currentFile.name, {
|
||||
type: "application/pdf",
|
||||
});
|
||||
} finally {
|
||||
closeDocAndFreeBuffer(m, docPtr);
|
||||
}
|
||||
const resultBytes = await embedSignatureImages(
|
||||
await signedFile.arrayBuffer(),
|
||||
allAnnotations,
|
||||
getImageData,
|
||||
);
|
||||
signedFile = new File([resultBytes as BlobPart], currentFile.name, {
|
||||
type: "application/pdf",
|
||||
});
|
||||
} catch (renderError) {
|
||||
console.error("Failed to manually render annotations:", renderError);
|
||||
console.warn("Signatures may only show as annotations");
|
||||
console.error("Failed to embed signature images:", renderError);
|
||||
console.warn("Signatures may only remain as annotations");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,16 +207,211 @@ export async function flattenSignatures(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Uint8Array PNG bytes to a data URL for canvas decoding.
|
||||
*/
|
||||
function uint8ArrayToPngDataUrl(pngBytes: Uint8Array): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const blob = new Blob([pngBytes as BlobPart], { type: "image/png" });
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
type SignatureAnnotationsByPage = Array<{
|
||||
pageIndex: number;
|
||||
annotations: any[];
|
||||
}>;
|
||||
|
||||
function extractImageDataUrl(
|
||||
value: unknown,
|
||||
depth = 0,
|
||||
visited: Set<unknown> = new Set(),
|
||||
): string | undefined {
|
||||
if (!value || depth > 6) return undefined;
|
||||
|
||||
if (typeof value === "string") {
|
||||
return value.startsWith("data:image") ? value : undefined;
|
||||
}
|
||||
|
||||
if (typeof value !== "object" || visited.has(value)) return undefined;
|
||||
visited.add(value);
|
||||
|
||||
const entries = Array.isArray(value)
|
||||
? value
|
||||
: Object.values(value as Record<string, unknown>);
|
||||
for (const entry of entries) {
|
||||
const imageDataUrl = extractImageDataUrl(entry, depth + 1, visited);
|
||||
if (imageDataUrl) return imageDataUrl;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getAnnotationImageData(
|
||||
annotation: any,
|
||||
getImageData: (id: string) => string | undefined,
|
||||
): string | undefined {
|
||||
// EmbedPDF can replace fields such as imageData/appearance with an internal
|
||||
// asset reference after placement. Prefer our persistent original and only
|
||||
// accept values that actually contain an image data URL.
|
||||
const candidates: unknown[] = [
|
||||
annotation.id ? getImageData(annotation.id) : undefined,
|
||||
annotation.imageSrc,
|
||||
annotation.imageData,
|
||||
annotation.appearance,
|
||||
annotation.stampData,
|
||||
annotation.contents,
|
||||
annotation.data,
|
||||
annotation.customData,
|
||||
annotation.asset,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const imageDataUrl = extractImageDataUrl(candidate);
|
||||
if (imageDataUrl) return imageDataUrl;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function embedSignatureImages(
|
||||
pdfArrayBuffer: ArrayBuffer,
|
||||
annotationsByPage: SignatureAnnotationsByPage,
|
||||
getImageData: (id: string) => string | undefined,
|
||||
imageDecoder: (
|
||||
dataUrl: string,
|
||||
) => Promise<DecodedImage | null> = decodeImageDataUrl,
|
||||
): Promise<ArrayBuffer> {
|
||||
const m = await getPdfiumModule();
|
||||
const docPtr = await openRawDocumentSafe(pdfArrayBuffer);
|
||||
|
||||
try {
|
||||
const pageCount = m.FPDF_GetPageCount(docPtr);
|
||||
|
||||
for (const { pageIndex, annotations } of annotationsByPage) {
|
||||
if (pageIndex < 0 || pageIndex >= pageCount) continue;
|
||||
|
||||
const pagePtr = m.FPDF_LoadPage(docPtr, pageIndex);
|
||||
if (!pagePtr) continue;
|
||||
|
||||
try {
|
||||
const pageBox = readEffectivePageBox(m, pagePtr);
|
||||
const cropHeight = pageBox.top - pageBox.bottom;
|
||||
|
||||
for (const annotation of annotations) {
|
||||
const rect =
|
||||
annotation.rect ??
|
||||
annotation.bounds ??
|
||||
annotation.rectangle ??
|
||||
annotation.position;
|
||||
if (!rect) continue;
|
||||
|
||||
const originalX = rect.origin?.x ?? rect.x ?? rect.left ?? 0;
|
||||
const originalY = rect.origin?.y ?? rect.y ?? rect.top ?? 0;
|
||||
const width = rect.size?.width ?? rect.width ?? 100;
|
||||
const height = rect.size?.height ?? rect.height ?? 50;
|
||||
if (width <= 0 || height <= 0) continue;
|
||||
|
||||
let imageDataUrl = getAnnotationImageData(annotation, getImageData);
|
||||
if (!imageDataUrl) continue;
|
||||
|
||||
if (imageDataUrl.startsWith("data:image/svg+xml")) {
|
||||
const pngBytes = await rasteriseSvgToPng(
|
||||
imageDataUrl,
|
||||
width * 2,
|
||||
height * 2,
|
||||
);
|
||||
if (!pngBytes) continue;
|
||||
imageDataUrl = `data:image/png;base64,${uint8ArrayToBase64(pngBytes)}`;
|
||||
}
|
||||
|
||||
const decodedImage = await imageDecoder(imageDataUrl);
|
||||
if (!decodedImage) continue;
|
||||
|
||||
const pdfX = pageBox.left + originalX;
|
||||
const pdfY = pageBox.bottom + cropHeight - originalY - height;
|
||||
appendStampAnnotation(
|
||||
m,
|
||||
docPtr,
|
||||
pagePtr,
|
||||
decodedImage,
|
||||
pdfX,
|
||||
pdfY,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
m.FPDF_ClosePage(pagePtr);
|
||||
}
|
||||
}
|
||||
|
||||
return await saveRawDocument(docPtr);
|
||||
} finally {
|
||||
closeDocAndFreeBuffer(m, docPtr);
|
||||
}
|
||||
}
|
||||
|
||||
const FPDF_ANNOT_STAMP = 13;
|
||||
const FPDF_ANNOT_FLAG_PRINT = 1 << 2;
|
||||
const FPDF_ANNOT_FLAG_READONLY = 1 << 6;
|
||||
const FPDF_ANNOT_FLAG_LOCKED = 1 << 7;
|
||||
|
||||
function appendStampAnnotation(
|
||||
m: Awaited<ReturnType<typeof getPdfiumModule>>,
|
||||
docPtr: number,
|
||||
pagePtr: number,
|
||||
image: DecodedImage,
|
||||
pdfX: number,
|
||||
pdfY: number,
|
||||
width: number,
|
||||
height: number,
|
||||
): boolean {
|
||||
const annotationIndex = m.FPDFPage_GetAnnotCount(pagePtr);
|
||||
const annotPtr = m.FPDFPage_CreateAnnot(pagePtr, FPDF_ANNOT_STAMP);
|
||||
if (!annotPtr) return false;
|
||||
|
||||
let appended = false;
|
||||
let imageObjPtr = 0;
|
||||
const rectPtr = m.pdfium.wasmExports.malloc(4 * 4);
|
||||
|
||||
try {
|
||||
// FS_RECTF layout: left, top, right, bottom.
|
||||
m.pdfium.setValue(rectPtr, pdfX, "float");
|
||||
m.pdfium.setValue(rectPtr + 4, pdfY + height, "float");
|
||||
m.pdfium.setValue(rectPtr + 8, pdfX + width, "float");
|
||||
m.pdfium.setValue(rectPtr + 12, pdfY, "float");
|
||||
if (!m.FPDFAnnot_SetRect(annotPtr, rectPtr)) return false;
|
||||
|
||||
imageObjPtr =
|
||||
createBitmapImageObject(
|
||||
m,
|
||||
docPtr,
|
||||
pagePtr,
|
||||
image,
|
||||
pdfX,
|
||||
pdfY,
|
||||
width,
|
||||
height,
|
||||
) ?? 0;
|
||||
if (!imageObjPtr) return false;
|
||||
|
||||
if (!m.FPDFAnnot_AppendObject(annotPtr, imageObjPtr)) return false;
|
||||
imageObjPtr = 0; // The annotation owns the object after a successful append.
|
||||
|
||||
m.FPDFAnnot_SetFlags(
|
||||
annotPtr,
|
||||
FPDF_ANNOT_FLAG_PRINT | FPDF_ANNOT_FLAG_READONLY | FPDF_ANNOT_FLAG_LOCKED,
|
||||
);
|
||||
appended = true;
|
||||
return true;
|
||||
} finally {
|
||||
m.pdfium.wasmExports.free(rectPtr);
|
||||
if (imageObjPtr) m.FPDFPageObj_Destroy(imageObjPtr);
|
||||
m.FPDFPage_CloseAnnot(annotPtr);
|
||||
if (!appended) m.FPDFPage_RemoveAnnot(pagePtr, annotationIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function uint8ArrayToBase64(bytes: Uint8Array): string {
|
||||
let binary = "";
|
||||
const chunkSize = 0x8000;
|
||||
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
|
||||
binary += String.fromCharCode(
|
||||
...bytes.subarray(offset, offset + chunkSize),
|
||||
);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,14 @@ export function calculateScaleFromFileSize(fileSize: number): number {
|
||||
/** PDFium error code 4 = password required (encrypted PDF). */
|
||||
const PDFIUM_ERR_PASSWORD = 4;
|
||||
|
||||
/** PDFs at or above this size never get a full-buffer client-side parse
|
||||
* (renderer OOM) - only the linearized-prefix attempt below. */
|
||||
export const LARGE_PDF_PARSE_LIMIT = 100 * 1024 * 1024;
|
||||
|
||||
/** Linearized PDFs keep page 1 + hint tables in the first bytes, so a small
|
||||
* prefix is often enough to render a thumbnail without reading the file. */
|
||||
const LINEARIZED_PREFIX_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
interface PdfiumRenderResult {
|
||||
thumbnail: string;
|
||||
pageCount: number;
|
||||
@@ -112,6 +120,72 @@ async function renderPdfThumbnailPdfium(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render both thumbnail variants (upright + rotation-baked) from a single
|
||||
* document open - halves the parse and memory cost of the add-files path.
|
||||
*/
|
||||
async function renderPdfThumbnailPairPdfium(
|
||||
data: ArrayBuffer,
|
||||
scale: number,
|
||||
collectAllPagesMetadata: boolean,
|
||||
): Promise<{ unrotated: PdfiumRenderResult; rotated: PdfiumRenderResult }> {
|
||||
const m = await getPdfiumModule();
|
||||
let docPtr: number;
|
||||
try {
|
||||
docPtr = await openRawDocumentSafe(data);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
new RegExp(`error ${PDFIUM_ERR_PASSWORD}`).test(error.message)
|
||||
) {
|
||||
const encrypted: PdfiumRenderResult = {
|
||||
thumbnail: "",
|
||||
pageCount: 1,
|
||||
pageRotations: [],
|
||||
pageDimensions: [],
|
||||
isEncrypted: true,
|
||||
};
|
||||
return { unrotated: encrypted, rotated: { ...encrypted } };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
const pageCount = m.FPDF_GetPageCount(docPtr);
|
||||
const unrotatedThumb = await renderPdfiumPageDataUrl(docPtr, 0, scale, {
|
||||
applyRotation: false,
|
||||
});
|
||||
const rotatedThumb = await renderPdfiumPageDataUrl(docPtr, 0, scale, {
|
||||
applyRotation: true,
|
||||
});
|
||||
if (!unrotatedThumb || !rotatedThumb) {
|
||||
throw new Error("PDFium: failed to render page 0");
|
||||
}
|
||||
|
||||
const firstMeta = await readPdfiumPageMetadata(docPtr, 0);
|
||||
const pageRotations: number[] = [firstMeta?.rotation ?? 0];
|
||||
const pageDimensions: Array<{ width: number; height: number }> = [
|
||||
{ width: firstMeta?.width ?? 0, height: firstMeta?.height ?? 0 },
|
||||
];
|
||||
if (collectAllPagesMetadata) {
|
||||
for (let i = 1; i < pageCount; i++) {
|
||||
const meta = await readPdfiumPageMetadata(docPtr, i);
|
||||
if (!meta) continue;
|
||||
pageRotations[i] = meta.rotation;
|
||||
pageDimensions[i] = { width: meta.width, height: meta.height };
|
||||
}
|
||||
}
|
||||
|
||||
const base = { pageCount, pageRotations, pageDimensions };
|
||||
return {
|
||||
unrotated: { thumbnail: unrotatedThumb, ...base },
|
||||
rotated: { thumbnail: rotatedThumb, ...base },
|
||||
};
|
||||
} finally {
|
||||
await closeRawDocument(docPtr);
|
||||
}
|
||||
}
|
||||
|
||||
async function generatePDFThumbnail(
|
||||
arrayBuffer: ArrayBuffer,
|
||||
scale: number,
|
||||
@@ -133,7 +207,7 @@ async function generatePDFThumbnail(
|
||||
*/
|
||||
export async function generateThumbnailForFile(file: File): Promise<string> {
|
||||
// Very large PDFs skip thumbnail generation — SVG icon shown in UI instead
|
||||
if (file.size >= 100 * 1024 * 1024) {
|
||||
if (file.size >= LARGE_PDF_PARSE_LIMIT) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -152,8 +226,7 @@ export async function generateThumbnailForFile(file: File): Promise<string> {
|
||||
const scale = calculateScaleFromFileSize(file.size);
|
||||
|
||||
// Only read first 2MB for thumbnail generation to save memory
|
||||
const chunkSize = 2 * 1024 * 1024; // 2MB
|
||||
const chunk = file.slice(0, Math.min(chunkSize, file.size));
|
||||
const chunk = file.slice(0, Math.min(LINEARIZED_PREFIX_BYTES, file.size));
|
||||
const arrayBuffer = await chunk.arrayBuffer();
|
||||
|
||||
try {
|
||||
@@ -193,6 +266,31 @@ export async function generateThumbnailWithMetadata(
|
||||
|
||||
const scale = calculateScaleFromFileSize(file.size);
|
||||
|
||||
// Never full-parse huge PDFs client-side - the renderer process OOMs long
|
||||
// before system RAM runs out. The prefix succeeds for linearized PDFs.
|
||||
if (file.size >= LARGE_PDF_PARSE_LIMIT) {
|
||||
try {
|
||||
const chunk = await file.slice(0, LINEARIZED_PREFIX_BYTES).arrayBuffer();
|
||||
const result = await renderPdfThumbnailPdfium(
|
||||
chunk,
|
||||
scale,
|
||||
applyRotation,
|
||||
false,
|
||||
);
|
||||
if (result.isEncrypted) {
|
||||
return { thumbnail: "", pageCount: 1, isEncrypted: true };
|
||||
}
|
||||
return {
|
||||
thumbnail: result.thumbnail,
|
||||
pageCount: result.pageCount,
|
||||
pageRotations: result.pageRotations,
|
||||
pageDimensions: result.pageDimensions,
|
||||
};
|
||||
} catch {
|
||||
return { thumbnail: "", pageCount: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
// Always read per-page rotation: PageEditor renders thumbnails upright and
|
||||
@@ -222,3 +320,41 @@ export async function generateThumbnailWithMetadata(
|
||||
return { thumbnail: "", pageCount: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Both thumbnail variants + page metadata from ONE full parse instead of two.
|
||||
* Large PDFs only get the linearized-prefix attempt; if that fails, both
|
||||
* variants are empty placeholders and page metadata is omitted.
|
||||
*/
|
||||
export async function generateThumbnailPairWithMetadata(file: File): Promise<{
|
||||
unrotated: ThumbnailWithMetadata;
|
||||
rotated: ThumbnailWithMetadata;
|
||||
}> {
|
||||
const scale = calculateScaleFromFileSize(file.size);
|
||||
try {
|
||||
const isLarge = file.size >= LARGE_PDF_PARSE_LIMIT;
|
||||
const buffer = isLarge
|
||||
? await file.slice(0, LINEARIZED_PREFIX_BYTES).arrayBuffer()
|
||||
: await file.arrayBuffer();
|
||||
const pair = await renderPdfThumbnailPairPdfium(buffer, scale, !isLarge);
|
||||
|
||||
const toPublic = (r: PdfiumRenderResult): ThumbnailWithMetadata =>
|
||||
r.isEncrypted
|
||||
? { thumbnail: "", pageCount: 1, isEncrypted: true }
|
||||
: {
|
||||
thumbnail: r.thumbnail,
|
||||
pageCount: r.pageCount,
|
||||
pageRotations: r.pageRotations,
|
||||
pageDimensions: r.pageDimensions,
|
||||
};
|
||||
return {
|
||||
unrotated: toPublic(pair.unrotated),
|
||||
rotated: toPublic(pair.rotated),
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
unrotated: { thumbnail: "", pageCount: 0 },
|
||||
rotated: { thumbnail: "", pageCount: 0 },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,20 @@ export function AppProviders({ children }: { children: ReactNode }) {
|
||||
const [appKey, setAppKey] = useState(0);
|
||||
const hasLoadedInitialMode = useRef(false);
|
||||
|
||||
// Files dropped outside a dropzone must never navigate the webview to the
|
||||
// file (Linux WebKit renders the PDF fullscreen and orphans the app UI).
|
||||
// Dropzone-level handlers run before these window-level listeners, so
|
||||
// in-app drag & drop is unaffected.
|
||||
useEffect(() => {
|
||||
const preventNavigation = (e: DragEvent) => e.preventDefault();
|
||||
window.addEventListener("dragover", preventNavigation);
|
||||
window.addEventListener("drop", preventNavigation);
|
||||
return () => {
|
||||
window.removeEventListener("dragover", preventNavigation);
|
||||
window.removeEventListener("drop", preventNavigation);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Load connection mode on mount and subscribe to future changes
|
||||
useEffect(() => {
|
||||
void connectionModeService.getCurrentMode().then((mode) => {
|
||||
|
||||
@@ -23,6 +23,7 @@ export const useConfigNavSections = (
|
||||
runningEE: boolean = false,
|
||||
loginEnabled: boolean = false,
|
||||
onRequestClose: () => void = () => {},
|
||||
showSettingsWhenNoLogin: boolean = true,
|
||||
): ConfigNavSection[] => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -53,6 +54,7 @@ export const useConfigNavSections = (
|
||||
runningEE,
|
||||
loginEnabled,
|
||||
onRequestClose,
|
||||
showSettingsWhenNoLogin,
|
||||
);
|
||||
|
||||
const connectionModeSection: ConfigNavSection = {
|
||||
|
||||
@@ -21,8 +21,6 @@ export function useExitWarning() {
|
||||
const handleCloseRequested = async (event: {
|
||||
preventDefault: () => void;
|
||||
}) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (isClosingRef.current) {
|
||||
return;
|
||||
}
|
||||
@@ -30,6 +28,15 @@ export function useExitWarning() {
|
||||
const allStubs = selectorsRef.current.getStirlingFileStubs();
|
||||
const dirtyStubs = allStubs.filter((stub) => stub.isDirty);
|
||||
|
||||
// Nothing unsaved: don't preventDefault, so the window closes natively
|
||||
// without depending on the JS dialog/destroy round-trip below.
|
||||
if (dirtyStubs.length === 0) {
|
||||
isClosingRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (dirtyStubs.length > 0) {
|
||||
const fileList = dirtyStubs.map((f) => `• ${f.name}`).join("\n");
|
||||
const saveLabel = t("confirmCloseSave", "Save and close");
|
||||
|
||||
@@ -18,6 +18,11 @@ export class TauriBackendService {
|
||||
private isRecovering = false;
|
||||
private restartAttempts = 0;
|
||||
private static readonly MAX_RESTART_ATTEMPTS = 3;
|
||||
/** Failed health checks before the first-ever success are "starting", not "unhealthy" -
|
||||
* a slow first boot must not trigger the restart/notification loop. */
|
||||
private hasBeenHealthy = false;
|
||||
private startupGraceUntil = 0;
|
||||
private static readonly STARTUP_GRACE_MS = 120_000;
|
||||
|
||||
static getInstance(): TauriBackendService {
|
||||
if (!TauriBackendService.instance) {
|
||||
@@ -115,9 +120,18 @@ export class TauriBackendService {
|
||||
// Reset started flag so startBackend() will run again
|
||||
this.backendStarted = false;
|
||||
this.startPromise = null;
|
||||
// Fresh grace window: the restarted backend needs boot time before failed
|
||||
// health checks may count as unhealthy again.
|
||||
this.hasBeenHealthy = false;
|
||||
this.setStatus("starting");
|
||||
try {
|
||||
await this.startBackend();
|
||||
// startBackend resolves once the port is known, not once Spring is up -
|
||||
// only declare success after a real health check passes.
|
||||
const healthy = await this.waitUntilHealthy(60_000);
|
||||
if (!healthy) {
|
||||
throw new Error("Backend did not become healthy after restart");
|
||||
}
|
||||
this.restartAttempts = 0; // Reset on successful restart
|
||||
this.isRecovering = false;
|
||||
console.log("[TauriBackendService] Backend restarted successfully.");
|
||||
@@ -156,6 +170,7 @@ export class TauriBackendService {
|
||||
}
|
||||
|
||||
this.backendStarted = true; // Mark as active for health checks
|
||||
this.startupGraceUntil = Date.now() + TauriBackendService.STARTUP_GRACE_MS;
|
||||
this.setStatus("starting");
|
||||
this.beginHealthMonitoring();
|
||||
|
||||
@@ -176,6 +191,7 @@ export class TauriBackendService {
|
||||
return this.startPromise;
|
||||
}
|
||||
|
||||
this.startupGraceUntil = Date.now() + TauriBackendService.STARTUP_GRACE_MS;
|
||||
this.setStatus("starting");
|
||||
|
||||
this.startPromise = invoke("start_backend", { backendUrl })
|
||||
@@ -260,7 +276,7 @@ export class TauriBackendService {
|
||||
console.warn(
|
||||
`[TauriBackendService] Health check failed: ${response.status}`,
|
||||
);
|
||||
this.setStatus("unhealthy");
|
||||
this.setStatus(this.isInStartupGrace() ? "starting" : "unhealthy");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -270,15 +286,22 @@ export class TauriBackendService {
|
||||
`[TauriBackendService] dependenciesReady=${dependenciesReady}`,
|
||||
);
|
||||
|
||||
if (dependenciesReady) {
|
||||
this.hasBeenHealthy = true;
|
||||
}
|
||||
this.setStatus(dependenciesReady ? "healthy" : "starting");
|
||||
return dependenciesReady;
|
||||
} catch (error) {
|
||||
console.error("[TauriBackendService] Health check error:", error);
|
||||
this.setStatus("unhealthy");
|
||||
this.setStatus(this.isInStartupGrace() ? "starting" : "unhealthy");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private isInStartupGrace(): boolean {
|
||||
return !this.hasBeenHealthy && Date.now() < this.startupGraceUntil;
|
||||
}
|
||||
|
||||
private async waitForHealthy(): Promise<void> {
|
||||
while (true) {
|
||||
const isHealthy = await this.checkBackendHealth();
|
||||
@@ -289,6 +312,17 @@ export class TauriBackendService {
|
||||
}
|
||||
}
|
||||
|
||||
private async waitUntilHealthy(timeoutMs: number): Promise<boolean> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
if (await this.checkBackendHealth()) {
|
||||
return true;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset backend state (used when switching from external to local backend)
|
||||
*/
|
||||
@@ -298,6 +332,8 @@ export class TauriBackendService {
|
||||
this.isLocalBackend = false;
|
||||
this.isRecovering = false;
|
||||
this.restartAttempts = 0;
|
||||
this.hasBeenHealthy = false;
|
||||
this.startupGraceUntil = 0;
|
||||
if (this.recoveryTimer) {
|
||||
clearTimeout(this.recoveryTimer);
|
||||
this.recoveryTimer = null;
|
||||
|
||||
@@ -32,6 +32,7 @@ export const useConfigNavSections = (
|
||||
runningEE: boolean = false,
|
||||
loginEnabled: boolean = false,
|
||||
onRequestClose: () => void = () => {},
|
||||
showSettingsWhenNoLogin: boolean = true,
|
||||
): ConfigNavSection[] => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -41,6 +42,7 @@ export const useConfigNavSections = (
|
||||
runningEE,
|
||||
loginEnabled,
|
||||
onRequestClose,
|
||||
showSettingsWhenNoLogin,
|
||||
);
|
||||
|
||||
// Add account management under Preferences
|
||||
@@ -64,8 +66,9 @@ export const useConfigNavSections = (
|
||||
}
|
||||
}
|
||||
|
||||
// Add Admin sections if user is admin OR if login is disabled (but mark as disabled)
|
||||
if (isAdmin || !loginEnabled) {
|
||||
// Add Admin sections for admins. When login is disabled, keep the historical
|
||||
// read-only admin preview only if system.showSettingsWhenNoLogin allows it.
|
||||
if (isAdmin || (!loginEnabled && showSettingsWhenNoLogin)) {
|
||||
const requiresLogin = !loginEnabled;
|
||||
const enableLoginTooltip = t(
|
||||
"settings.tooltips.enableLoginFirst",
|
||||
|
||||
+1
-1
@@ -482,7 +482,7 @@ export default function AdminGeneralSection() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb={4}>
|
||||
<Text component="div" size="sm" fw={500} mb={4}>
|
||||
<Group gap="xs">
|
||||
<span>
|
||||
{t("admin.settings.general.logoStyle.label", "Logo Style")}
|
||||
|
||||
@@ -48,7 +48,7 @@ const FREE_LICENSE_INFO: LicenseInfo = {
|
||||
|
||||
const BASE_NO_LOGIN_CONFIG: AppConfig = {
|
||||
enableAnalytics: true,
|
||||
appVersion: "2.14.2",
|
||||
appVersion: "2.14.3",
|
||||
serverCertificateEnabled: false,
|
||||
enableAlphaFunctionality: false,
|
||||
enableDesktopInstallSlide: true,
|
||||
|
||||
Generated
+163
-163
@@ -12,29 +12,29 @@
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@cantoo/pdf-lib": "^2.5.3",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@embedpdf/core": "^2.14.1",
|
||||
"@embedpdf/engines": "^2.8.0",
|
||||
"@embedpdf/models": "^2.14.1",
|
||||
"@embedpdf/plugin-annotation": "^2.14.1",
|
||||
"@embedpdf/plugin-attachment": "^2.14.1",
|
||||
"@embedpdf/plugin-bookmark": "^2.14.1",
|
||||
"@embedpdf/plugin-document-manager": "^2.14.1",
|
||||
"@embedpdf/plugin-export": "^2.14.1",
|
||||
"@embedpdf/plugin-history": "^2.14.1",
|
||||
"@embedpdf/plugin-interaction-manager": "^2.14.1",
|
||||
"@embedpdf/plugin-pan": "^2.14.1",
|
||||
"@embedpdf/plugin-print": "^2.14.1",
|
||||
"@embedpdf/plugin-redaction": "^2.14.1",
|
||||
"@embedpdf/plugin-render": "^2.14.1",
|
||||
"@embedpdf/plugin-rotate": "^2.14.1",
|
||||
"@embedpdf/plugin-scroll": "^2.14.1",
|
||||
"@embedpdf/plugin-search": "^2.14.1",
|
||||
"@embedpdf/plugin-selection": "^2.8.0",
|
||||
"@embedpdf/plugin-spread": "^2.14.1",
|
||||
"@embedpdf/plugin-thumbnail": "^2.14.1",
|
||||
"@embedpdf/plugin-tiling": "^2.14.1",
|
||||
"@embedpdf/plugin-viewport": "^2.14.1",
|
||||
"@embedpdf/plugin-zoom": "^2.14.1",
|
||||
"@embedpdf/core": "^2.14.4",
|
||||
"@embedpdf/engines": "^2.14.4",
|
||||
"@embedpdf/models": "^2.14.4",
|
||||
"@embedpdf/plugin-annotation": "^2.14.4",
|
||||
"@embedpdf/plugin-attachment": "^2.14.4",
|
||||
"@embedpdf/plugin-bookmark": "^2.14.4",
|
||||
"@embedpdf/plugin-document-manager": "^2.14.4",
|
||||
"@embedpdf/plugin-export": "^2.14.4",
|
||||
"@embedpdf/plugin-history": "^2.14.4",
|
||||
"@embedpdf/plugin-interaction-manager": "^2.14.4",
|
||||
"@embedpdf/plugin-pan": "^2.14.4",
|
||||
"@embedpdf/plugin-print": "^2.14.4",
|
||||
"@embedpdf/plugin-redaction": "^2.14.4",
|
||||
"@embedpdf/plugin-render": "^2.14.4",
|
||||
"@embedpdf/plugin-rotate": "^2.14.4",
|
||||
"@embedpdf/plugin-scroll": "^2.14.4",
|
||||
"@embedpdf/plugin-search": "^2.14.4",
|
||||
"@embedpdf/plugin-selection": "^2.14.4",
|
||||
"@embedpdf/plugin-spread": "^2.14.4",
|
||||
"@embedpdf/plugin-thumbnail": "^2.14.4",
|
||||
"@embedpdf/plugin-tiling": "^2.14.4",
|
||||
"@embedpdf/plugin-viewport": "^2.14.4",
|
||||
"@embedpdf/plugin-zoom": "^2.14.4",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@iconify/react": "^6.0.2",
|
||||
@@ -719,13 +719,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/core": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-2.14.1.tgz",
|
||||
"integrity": "sha512-L0lNn5WGnGDPaI1q2wnavVF6C6haRtvPGbMfqhZrjr7V+e0GExeoTO0VjK1DwH4IIBHOuD6nsQFYI89+nY/sJA==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-2.14.4.tgz",
|
||||
"integrity": "sha512-trrFhKePk1JnAS8Vd+NWrS1fU2SIj3sifwawho/MeWKTkferLLuBay96ccA4S/Q5D7glKNH//M58VRgns09P3A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/engines": "2.14.1",
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/engines": "2.14.4",
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"preact": "^10.26.4",
|
||||
@@ -736,9 +736,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/engines": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-2.14.1.tgz",
|
||||
"integrity": "sha512-k+HHuhj7dPKZC+8wUvMbpEUbNfi6F5r96Ky2XbNhBxiMQSWlVljBQVfZWcq9Jy3TLslwn5m2BbrvvHw+UVqe3w==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-2.14.4.tgz",
|
||||
"integrity": "sha512-1kWYvrxjpKr5QUt16r93pLQOLoWEuzD447OViwZyRECsZ8ePoTnErlNlxHiTkeAWBgwr4DD7Xd+2VkTdMAacNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/fonts-arabic": "1.0.0",
|
||||
@@ -748,8 +748,8 @@
|
||||
"@embedpdf/fonts-latin": "1.0.0",
|
||||
"@embedpdf/fonts-sc": "1.0.0",
|
||||
"@embedpdf/fonts-tc": "1.0.0",
|
||||
"@embedpdf/models": "2.14.1",
|
||||
"@embedpdf/pdfium": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4",
|
||||
"@embedpdf/pdfium": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"preact": "^10.26.4",
|
||||
@@ -802,32 +802,32 @@
|
||||
"license": "OFL-1.1"
|
||||
},
|
||||
"node_modules/@embedpdf/models": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-2.14.1.tgz",
|
||||
"integrity": "sha512-jbuoXKv4jW8F1o8EzCmPIB5U/yVC5DWlWhxL9ZfvBiBgW2HIy68ydcu+AykqOG75eAvCfxqcvF2HzSDsV3+K0A==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-2.14.4.tgz",
|
||||
"integrity": "sha512-RlxazYdS1ObWe3Rt78pVUXSqQEMeOb1oHFA0CT3aolwtVooZyop9XO3WTDWpEaGvGrHXvuSv7WQgcCLZ2zKyaQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@embedpdf/pdfium": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-2.14.1.tgz",
|
||||
"integrity": "sha512-4FKhpeb7CYfkZ1k0WyDTq8i2FlIO5MaR0ywIDJ0goxOjenKkYOgHQ4747B5evxRzNEC2H5V3Rk6Dunf7TlIYBQ==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-2.14.4.tgz",
|
||||
"integrity": "sha512-0tDPQEH1WtfXcASNVp5U3dqfzZboNvcE4rDok/P4JCN+DCTaJDG2E9lYHGMo9FtuS2IdHgrzkmPRpOo5WV9MKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-annotation": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-2.14.1.tgz",
|
||||
"integrity": "sha512-YGe/DLL5r9HsF54QYgNnsc82W4E2jXRaX6D4WzR38argBhiSLf3IhTJmFXVR2WlPK4VwW6DA0p7WqKcEYUOyDg==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-2.14.4.tgz",
|
||||
"integrity": "sha512-ff7rczmrBUoZFXlxC3fT6pSciMUuFAySOxY27oyM1IO3m2QLmzz5Xt9yu3oNYsanRPuO5a8nsgaifd9o8HheNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1",
|
||||
"@embedpdf/utils": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4",
|
||||
"@embedpdf/utils": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/plugin-history": "2.14.1",
|
||||
"@embedpdf/plugin-interaction-manager": "2.14.1",
|
||||
"@embedpdf/plugin-scroll": "2.14.1",
|
||||
"@embedpdf/plugin-selection": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"@embedpdf/plugin-history": "2.14.4",
|
||||
"@embedpdf/plugin-interaction-manager": "2.14.4",
|
||||
"@embedpdf/plugin-scroll": "2.14.4",
|
||||
"@embedpdf/plugin-selection": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -836,15 +836,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-attachment": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-attachment/-/plugin-attachment-2.14.1.tgz",
|
||||
"integrity": "sha512-RdR43Kp/t15/KcvAiN8VEMxhqJcTfK2ZeU95PbPHr8zZ1rDatRltVGmKQ4fcaBXQMv15VLTEPZNbGFbfRgdF8g==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-attachment/-/plugin-attachment-2.14.4.tgz",
|
||||
"integrity": "sha512-ktcv48w5DQVG7LTTt45rsth5X4dyxaXZb5zS1bElF1ULp6g7SRy4a0s3UWBY/UDKbpVtrby3JTsfs5fMU2VaRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -853,15 +853,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-bookmark": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-bookmark/-/plugin-bookmark-2.14.1.tgz",
|
||||
"integrity": "sha512-ur5tEu4OaVec8T6qF9c18W9hfMNqg0g6dkVqcuh+qaLUYznJpyxMSsNTMJmw/h607bsVLfA1QCwk4l2vORwNmQ==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-bookmark/-/plugin-bookmark-2.14.4.tgz",
|
||||
"integrity": "sha512-SfFhVsqEA+5rJUQSVTP259+cgMqbWR93G/kK6GhDcXou4jNGKNfmiWLeWSMp8lsnGhbMkWxFYJKLuH44NEm63g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -870,15 +870,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-document-manager": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-document-manager/-/plugin-document-manager-2.14.1.tgz",
|
||||
"integrity": "sha512-Qdj6Xkmpyt0aLHCkqZbOsOK5xzwI4IjaV8C2ihPiggB/iPDhoyXPklJb3wXmJfxjTIl+aw+FZmmPkmr4IpYn7w==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-document-manager/-/plugin-document-manager-2.14.4.tgz",
|
||||
"integrity": "sha512-1qUKu/RAN9LV+Ih7geig0X/2i4jIn8P1D0I0nQUwSMM9hJmitcyDJ/9SK/eOSsdEs4SulIqt+a2W2IMZOY2Vew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -887,15 +887,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-export": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-2.14.1.tgz",
|
||||
"integrity": "sha512-O52WPexZD7dbUJQnjp3ikhNg7D3PmGZJowJmXVcmJ2YlTkBabN799LBppC+FYP7nXrtbwy7UmG6vdGWha1b/NQ==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-2.14.4.tgz",
|
||||
"integrity": "sha512-z1IGsGzWve+g81KCrNbZYN/jHkb9mImodVhTfL35mJfOjOahRmbN0OUn1TvcNHaiXKjy8xcfabvxHDnSaViGlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -904,15 +904,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-history": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-2.14.1.tgz",
|
||||
"integrity": "sha512-mJUKNO69b8Cf0zpE3zJRM5in/AsVRLZq4bRbuAfK9c5+YG2Z59fYEjfRNSwDefxatzFTgy3mlZ5/fSfZsAZz0w==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-2.14.4.tgz",
|
||||
"integrity": "sha512-X6SBfpyKgQTHNmawoZOfeTW/nhE3NUeqd+gPLFCUkeVTaYO31qSeje3Z7z7pm17dMp5ar9TsgT69ajn6zlsG1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -921,15 +921,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-interaction-manager": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-2.14.1.tgz",
|
||||
"integrity": "sha512-eyH45cF3vHeZ5BlcgIjH212EtUhi20FvKSRZeLWZW72noOn6saXRDScLy/XGbtfGFjJoTrzkNA553hvrRpqVKA==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-2.14.4.tgz",
|
||||
"integrity": "sha512-y4MeHBy5s/SR5ma4rezYADIUujViAZSQe8A/CKehve4eU6Z9YF2AflL/M6KG24gCsUyW4D04AxAn3bvsXLR3nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -938,17 +938,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-pan": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-2.14.1.tgz",
|
||||
"integrity": "sha512-sHc9gjfoWQhfMcJ2JULsTzH9Sg9z6wX76Qvr5cujSPoOVV5bZ4QCB0kF1vgLFFzA1ia7vIBveGtDHIGkQQjsxA==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-2.14.4.tgz",
|
||||
"integrity": "sha512-TAVGzXg9qgfiZHZfZFkHry9jCSQjk/borV1DB8HJTcgTk52phhaSr4EFjBZ808Hj5t+cc9VZWoE1JhL8AAiKfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/plugin-interaction-manager": "2.14.1",
|
||||
"@embedpdf/plugin-viewport": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"@embedpdf/plugin-interaction-manager": "2.14.4",
|
||||
"@embedpdf/plugin-viewport": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -957,15 +957,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-print": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-print/-/plugin-print-2.14.1.tgz",
|
||||
"integrity": "sha512-wYuGuxuBhau7l2BfTlD5oohboRdseOn9/v0uVNGwKDuLHUTrv2WnkpzibBYIjv73gttyMBxTXdB3PZfnWOo8VA==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-print/-/plugin-print-2.14.4.tgz",
|
||||
"integrity": "sha512-qk1aQhLS+8eBL6lcT46p+fZaNjvLMVQF2Nl/t78aOy+cNskXHeYsnZi9tJKNYGLHbi+8+Dg2iHVIhFvweFn/Ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0",
|
||||
@@ -974,20 +974,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-redaction": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-redaction/-/plugin-redaction-2.14.1.tgz",
|
||||
"integrity": "sha512-xlcOgYDNqIKp9u7exT95RtfDGCkW1b+knGikpTNZEA5OJgtYO9RxquTkEZ4rdVZbgIWxs0xiMg6b0mhr33z4JQ==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-redaction/-/plugin-redaction-2.14.4.tgz",
|
||||
"integrity": "sha512-pECosjyBaLlYz1Fgde4gqquV4uQY8LAkbx7TtMwP1EhBFtG0QeoeuhytoLm9I/cWPi84jfaQjkWZB58HPxR5Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1",
|
||||
"@embedpdf/utils": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4",
|
||||
"@embedpdf/utils": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/plugin-annotation": "2.14.1",
|
||||
"@embedpdf/plugin-history": "2.14.1",
|
||||
"@embedpdf/plugin-interaction-manager": "2.14.1",
|
||||
"@embedpdf/plugin-selection": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"@embedpdf/plugin-annotation": "2.14.4",
|
||||
"@embedpdf/plugin-history": "2.14.4",
|
||||
"@embedpdf/plugin-interaction-manager": "2.14.4",
|
||||
"@embedpdf/plugin-selection": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -996,15 +996,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-render": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-2.14.1.tgz",
|
||||
"integrity": "sha512-KRjeSZeQRrg6fuyUgSU43yE6bOrRredpEiAfPI/FUCGgniO9nokTHjnFYg5G3UtVteJdnzdnKwzBl5sY++NQkg==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-2.14.4.tgz",
|
||||
"integrity": "sha512-KNY3EGYf9SRqtDBOEO9mR3Hyv9mN8TaC3ztERj0BX8zra7mkjkQnhGQlMxif1dGOoTD9XIcPqJvaw8o7PWDJDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -1013,15 +1013,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-rotate": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-2.14.1.tgz",
|
||||
"integrity": "sha512-ILu0Y6bXzLQg63cj9pAJ0ime5m5HVDOSBOyTT/Nb47hBfDLSbTWQuvAHZXwxPhGOjM5Pn6gHssKWZjW2WiXkBQ==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-2.14.4.tgz",
|
||||
"integrity": "sha512-+bBBHfoSMBMjHubDgPkCCu3o2NZn5vv8l/2gmzH6D8k90UVLB2AOei9VTmfy5xqoit+eoY6iSl7w3qyijNfB0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -1030,16 +1030,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-scroll": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-2.14.1.tgz",
|
||||
"integrity": "sha512-ppnP4t43EVNiXr2coEwybQ7BYucbUEpdTGlcEBm38DhLywgseVVySrcIMx7rHdM7ZylsW7uwS84hCFndTpPezg==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-2.14.4.tgz",
|
||||
"integrity": "sha512-0JgPwnQqB9THKJt/mOubgFkiTcCkOI6kFCZz8zlwl88DAJcUbdhJoTwCJZ2STYSfECTLB4ZFjb5fZ9AXyfkf0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/plugin-viewport": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"@embedpdf/plugin-viewport": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -1048,15 +1048,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-search": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-2.14.1.tgz",
|
||||
"integrity": "sha512-dAX4aIc4WeO+bUImbcHVm4c32UADYelpTeJA0It2eq6agZVND20B25kyDYpJiWKTl9NaVcJbNic+wreudl6vsQ==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-2.14.4.tgz",
|
||||
"integrity": "sha512-ErRlg0MKLxZ0c4OKk5RjPOV8f8yCaqFi6zqUxBI49/3yN6wDINKvm8dxt5Q2QGUa740n/XylT5BTbjH6lAjCzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -1065,17 +1065,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-selection": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-2.14.1.tgz",
|
||||
"integrity": "sha512-M+omnIDqgME+QgYX3nW3ypjzRvvz2IrSWpx7vP7vUhfd12R4ZJnTkS9AkKbXgHBVwWrxnbibKlZduiiehRLeKg==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-2.14.4.tgz",
|
||||
"integrity": "sha512-8w6ZUdwRcBhNCQjlS8mFvsMXI/wr1Q7DljwlFmKaEQyM9vXm2rI9WGN0z9ffjKHlsYn0yg98pqyPLtTevH1P7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1",
|
||||
"@embedpdf/utils": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4",
|
||||
"@embedpdf/utils": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/plugin-interaction-manager": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"@embedpdf/plugin-interaction-manager": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -1084,15 +1084,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-spread": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-2.14.1.tgz",
|
||||
"integrity": "sha512-MychoG54eYqjB+8/1/ArKYF+4eonULtBuGPqU72MO9t/Nadl8nNThaPrOW/eKQr3zQu1Af3KO7ng6tm6Umz4rA==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-2.14.4.tgz",
|
||||
"integrity": "sha512-ybJNbOMXBciD9cS+wwAUM1L5ZZMgF0+8Tpi78MaXGwnwH1wJUSx1Tx90YNsO9DxIKKckDsxUQREwUtTTcwFLEA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -1101,16 +1101,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-thumbnail": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-2.14.1.tgz",
|
||||
"integrity": "sha512-NOaFve83gbD6BUiANSSbiREjUJ1qpNyQrHl2RdMDNLyDaSUARweUu5zC5vfFRd9I+jUf3XGsCa3GNFlzQt+C9A==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-2.14.4.tgz",
|
||||
"integrity": "sha512-80D+R2uXoTsmQXFpGGhtapv3I45WNzTSIkcD3VvLywjS2MHwgCNY51RQsn5bc3HINZW9d/SX1F39qiv4HzPp6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/plugin-render": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"@embedpdf/plugin-render": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -1119,18 +1119,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-tiling": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-2.14.1.tgz",
|
||||
"integrity": "sha512-tvs3pPT6wVh0MjMN7Dp8R1XeoTjguH30j/ToWz9ziTKkwXADfJXdyjqYGhagiamMMTwQeUF9py+yDTMP23QV7Q==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-2.14.4.tgz",
|
||||
"integrity": "sha512-++1bzvU0Xqi6a1XeUOLAHU7D/FVUNobC/behSSn3WZnY/wIpYG3dXs5NbhNzeomIGXYHC4jCXbKGBPe5TNdoSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/plugin-render": "2.14.1",
|
||||
"@embedpdf/plugin-scroll": "2.14.1",
|
||||
"@embedpdf/plugin-viewport": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"@embedpdf/plugin-render": "2.14.4",
|
||||
"@embedpdf/plugin-scroll": "2.14.4",
|
||||
"@embedpdf/plugin-viewport": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -1139,15 +1139,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-viewport": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-2.14.1.tgz",
|
||||
"integrity": "sha512-U45B46XoaAFxC3yKnVhBO8Z5Wa+ca17UW7esJC/yWneJ2dNfSnvRAR7309KKP/RpZfDmn/lfJDVOFGCDbR3hvw==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-2.14.4.tgz",
|
||||
"integrity": "sha512-ZhbDITPZ/h1Vd9cUbCPShSbH5RXd/biCw9w6fneTItruMauYIcTNUS2eWLJC0Yovhw7nu0TjRVYuTQ71jzs0dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -1156,17 +1156,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-zoom": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-2.14.1.tgz",
|
||||
"integrity": "sha512-rVah7XgrWiNe9kT7VGRhNp5mCImf3eMk4cGO9kXIidQWka+yFqBANJw6Rj+FJdtWqz6pamZNWXTDZ1jvQkMKZg==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-2.14.4.tgz",
|
||||
"integrity": "sha512-a5kUjbG9sKMyO8qqDTrtL30VJ3dSEyNHHvszhfbvKtv4hppz/fSKS78vieu3lw48K11BoioImzCAF6Y3R0LMiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.14.1"
|
||||
"@embedpdf/models": "2.14.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.14.1",
|
||||
"@embedpdf/plugin-scroll": "2.14.1",
|
||||
"@embedpdf/plugin-viewport": "2.14.1",
|
||||
"@embedpdf/core": "2.14.4",
|
||||
"@embedpdf/plugin-scroll": "2.14.4",
|
||||
"@embedpdf/plugin-viewport": "2.14.4",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -1175,9 +1175,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/utils": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-2.14.1.tgz",
|
||||
"integrity": "sha512-TlJBT+SgRgcSMKH2EX3WBXIm/a/LEtgG2IiZ7hXPhT3t2e1HOeXjkvreJi6wcnvkMt4eYtJh3nocetto5gNclw==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-2.14.4.tgz",
|
||||
"integrity": "sha512-W7BZSNAQeUPQhi6l96Q4oFtg8Vgbt622LQL5ql0oZjyJNJRbYqCJIiJpR+5ZvK6S/x2MadZnYSzBJ/ekL6394A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"preact": "^10.26.4",
|
||||
|
||||
+23
-23
@@ -8,29 +8,29 @@
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@cantoo/pdf-lib": "^2.5.3",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@embedpdf/core": "^2.14.1",
|
||||
"@embedpdf/engines": "^2.8.0",
|
||||
"@embedpdf/models": "^2.14.1",
|
||||
"@embedpdf/plugin-annotation": "^2.14.1",
|
||||
"@embedpdf/plugin-attachment": "^2.14.1",
|
||||
"@embedpdf/plugin-bookmark": "^2.14.1",
|
||||
"@embedpdf/plugin-document-manager": "^2.14.1",
|
||||
"@embedpdf/plugin-export": "^2.14.1",
|
||||
"@embedpdf/plugin-history": "^2.14.1",
|
||||
"@embedpdf/plugin-interaction-manager": "^2.14.1",
|
||||
"@embedpdf/plugin-pan": "^2.14.1",
|
||||
"@embedpdf/plugin-print": "^2.14.1",
|
||||
"@embedpdf/plugin-redaction": "^2.14.1",
|
||||
"@embedpdf/plugin-render": "^2.14.1",
|
||||
"@embedpdf/plugin-rotate": "^2.14.1",
|
||||
"@embedpdf/plugin-scroll": "^2.14.1",
|
||||
"@embedpdf/plugin-search": "^2.14.1",
|
||||
"@embedpdf/plugin-selection": "^2.8.0",
|
||||
"@embedpdf/plugin-spread": "^2.14.1",
|
||||
"@embedpdf/plugin-thumbnail": "^2.14.1",
|
||||
"@embedpdf/plugin-tiling": "^2.14.1",
|
||||
"@embedpdf/plugin-viewport": "^2.14.1",
|
||||
"@embedpdf/plugin-zoom": "^2.14.1",
|
||||
"@embedpdf/core": "^2.14.4",
|
||||
"@embedpdf/engines": "^2.14.4",
|
||||
"@embedpdf/models": "^2.14.4",
|
||||
"@embedpdf/plugin-annotation": "^2.14.4",
|
||||
"@embedpdf/plugin-attachment": "^2.14.4",
|
||||
"@embedpdf/plugin-bookmark": "^2.14.4",
|
||||
"@embedpdf/plugin-document-manager": "^2.14.4",
|
||||
"@embedpdf/plugin-export": "^2.14.4",
|
||||
"@embedpdf/plugin-history": "^2.14.4",
|
||||
"@embedpdf/plugin-interaction-manager": "^2.14.4",
|
||||
"@embedpdf/plugin-pan": "^2.14.4",
|
||||
"@embedpdf/plugin-print": "^2.14.4",
|
||||
"@embedpdf/plugin-redaction": "^2.14.4",
|
||||
"@embedpdf/plugin-render": "^2.14.4",
|
||||
"@embedpdf/plugin-rotate": "^2.14.4",
|
||||
"@embedpdf/plugin-scroll": "^2.14.4",
|
||||
"@embedpdf/plugin-search": "^2.14.4",
|
||||
"@embedpdf/plugin-selection": "^2.14.4",
|
||||
"@embedpdf/plugin-spread": "^2.14.4",
|
||||
"@embedpdf/plugin-thumbnail": "^2.14.4",
|
||||
"@embedpdf/plugin-tiling": "^2.14.4",
|
||||
"@embedpdf/plugin-viewport": "^2.14.4",
|
||||
"@embedpdf/plugin-zoom": "^2.14.4",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@iconify/react": "^6.0.2",
|
||||
|
||||
@@ -23,29 +23,6 @@ plugins {
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||
}
|
||||
|
||||
// Depot remote build cache. Silently no-ops when DEPOT_TOKEN is absent
|
||||
// (local dev without depot login, and fork PRs where GitHub hides secrets),
|
||||
// so contributors without Depot access still build fine on local cache only.
|
||||
buildCache {
|
||||
def depotToken = System.getenv('DEPOT_TOKEN')
|
||||
local {
|
||||
enabled = true
|
||||
}
|
||||
if (depotToken) {
|
||||
remote(HttpBuildCache) {
|
||||
url = 'https://cache.depot.dev'
|
||||
enabled = true
|
||||
// Only CI runs push to the shared cache; dev laptops pull-only
|
||||
// so a misconfigured local task can't poison everyone else.
|
||||
push = System.getenv('CI') == 'true'
|
||||
credentials {
|
||||
username = ''
|
||||
password = depotToken
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = 'Stirling PDF'
|
||||
|
||||
// Flavors: core | proprietary (default) | saas.
|
||||
|
||||
Reference in New Issue
Block a user