Compare commits

..
Author SHA1 Message Date
Andres Gonzalez 1a8784635f 📚 Update user guide for upcoming release features
Document stroke to path, toolbar flyouts, line and arrow tools,
font previews, page multi-select, and dashboard grid/list view.

Relates to #9145, #10403, #10580, #10691
2026-09-08 16:09:04 +02:00
Andrey Antukh 41ec134ea2 📚 Update changelog 2026-09-01 12:02:57 +02:00
Andrey Antukh 9051b87f0e 📎 Update the update-changelog skill 2026-09-01 12:02:19 +02:00
Andrey Antukh a1079cf788 ⬆️ Update JVM, pnpm and node dependencies (#11404)
* ⬆️ Update pnpm and its deps

* ⬆️ Update JVM dependencies in backend and common

Update several JVM dependencies across backend and common:

- passay 1.6.6 -> 2.0.0 (package reorg, ctor-based rules)
- siphash 2.0.0 -> 3.0.0 (SipHasher* renamed to SipHash*)
- lettuce-core, guava, sqlite-jdbc, jsoup, lz4-java, markdown-clj,
  awssdk s3/sts, selmer, jackson-core/databind, shadow-cljs

Adapt passay validation to the new API (moved packages, constructor
configuration) and siphash to the renamed classes. Add tests for
password validation and UUID advisory-lock hashing.

AI-assisted-by: deepseek-v4-flash

* ⬆️ Update node on docker images

* 📎 Minor fixes related to pnpm12 compatibility
2026-09-01 12:01:35 +02:00
Eva Marco 5c4b35e350 🐛 Fix top spacing on left sidebar (#11446) 2026-09-01 10:56:53 +02:00
Andrey Antukh 23869f3b1b 🐛 Gate MCP REPL server behind isDevEnv check (#11282)
* 🐛 Gate MCP REPL server behind isDevEnv check

The ReplServer was starting unconditionally on every MCP server
instance, regardless of configuration. This exposed an
unauthenticated POST /execute endpoint that forwarded arbitrary
JavaScript to connected Penpot plugins.

Gate ReplServer creation, startup, and shutdown behind isDevEnv(),
consistent with how CljsReplTool and other dev tools are already
protected. Log an info message when the REPL server is disabled.

Consolidate the dev-env check into a single static isDevEnvEnabled()
method that isDevEnv() delegates to, avoiding duplicate logic. Add
PluginBridge.close() for proper WebSocket server cleanup on shutdown.

Add regression tests that construct PenpotMcpServer and verify
hasReplServer() returns the correct value based on the dev-env flag.

AI-assisted-by: mimo-v2.5-pro

*  Add PENPOT_MCP_REPL_ENABLE env var for explicit REPL control

Allow the REPL server to be enabled independently of the devenv
setting via a new PENPOT_MCP_REPL_ENABLE environment variable.
When set to "true", the REPL server starts regardless of
PENPOT_MCP_DEVENV; when set to any other value, it is disabled.
When unset, the previous isDevEnv fallback applies.

Addresses review feedback on PR #11282.

AI-assisted-by: mimo-v2.5-pro
2026-09-01 08:50:05 +02:00
Andrey Antukh 810f8ef671 🐛 Cascade profile deletion in objects-gc task (#11395)
The objects-gc task was performing a hard delete on profiles
without cascading the soft-delete to owned teams, projects, and files.
This left orphaned objects that were never cleaned up.

Now the task invokes delete-object before the hard delete, ensuring
all owned resources are properly marked for deletion and cleaned up
in subsequent GC iterations.

AI-assisted-by: qwen3.7-plus
2026-09-01 08:49:18 +02:00
Andrey Antukh 6d9f411fab 🐛 Enforce share-link comment permissions and page scope (#11371)
Fix two security vulnerabilities in comment RPCs when accessed
via share-links:

- GHSA-4p97-v4wg-jxfx: Share-link holders with who-comment=team
  could bypass the restriction and comment. The check-comment-permissions!
  function treated can-read as sufficient, but share-links always set
  can-read=true.

- GHSA-fwm4-hm9f-rmcp: Comment query RPCs returned threads from all
  pages, ignoring the share-link's :pages restriction.

Changes:
- files.clj: Differentiate :membership vs :share-link in
  check-comment-permissions!. For share-links, require
  has-comment-permissions? only (who-comment=all).
- comments.clj: Filter threads by (:pages perms) for share-link
  access in get-comment-threads, get-comment-thread, and get-comments.

Closes #11370

AI-assisted-by: qwen3.7-plus
2026-09-01 08:48:55 +02:00
Andrey Antukh 45f0153e8f 🐛 Prevent admin from removing team owner in delete-team-member (#11368)
Add owner protection to ::delete-team-member RPC command.
Previously, a team admin could remove the team owner, permanently
locking them out of their team and all resources.

Changes:
- Fetch target member data before deletion
- Validate member exists (return :not-found if not)
- Reject removal if target is owner and caller is not owner

This mirrors the existing protection in update-team-member-role.

Closes #11367

AI-assisted-by: qwen3.7-plus
2026-09-01 08:48:09 +02:00
Andrey Antukh 326d83e780 🐛 Block IPv6 transition addresses in SSRF guard (#11320)
* 🐛 Block IPv6 transition addresses in SSRF guard

The outbound HTTP SSRF blocklist did not classify NAT64
(64:ff9b::/96), 6to4 (2002::/16) or Teredo (2001:0000::/32)
addresses, whose embedded IPv4 target is invisible to the JVM
InetAddress predicates, so URLs resolving to them could reach cloud
metadata, loopback or RFC 1918 hosts from webhook delivery and media
import.

Transition ranges are now rejected outright and any embedded IPv4 is
re-checked against the full blocklist, including operator-supplied
extra blocked CIDRs.

Closes #11319

* ♻️ Remove dead embedded-IPv4 re-check from SSRF guard

The previous commit added a recursive re-check of the IPv4 embedded in
NAT64/6to4/Teredo addresses, but the `or` in `blocked-address?`
short-circuits on the truthy keyword returned by `transition-prefix`,
so the embedded-IPv4 branch was unreachable. The transition ranges are
already rejected outright (fail-closed), making the re-check both
unnecessary and untested.

Remove `transition-embedded-ipv4`, simplify the IPv6 branch to a plain
prefix check, and correct the docstrings and tests to match what the
code actually does.

AI-assisted-by: glm-5.3-flash
2026-09-01 08:47:46 +02:00
Andrey Antukh 15195b3bbb 🐛 Filter share-link tokens in get-view-only-bundle response (#11286)
* 🐛 Filter share-link tokens in get-view-only-bundle response

The get-view-only-bundle RPC command returned all share-link tokens for a file, allowing an anonymous holder of a restrictive share-link to enumerate and use more permissive tokens.

When authenticating via a share-link, the response now only includes the share-link used for authentication, preventing token disclosure and scope escalation.

Implemented using TDD:
- RED: Test demonstrates vulnerability (all tokens visible)
- GREEN: Filter share-links when (:type perms) = :share-link
- Verified all existing tests still pass

Closes #11285

AI-assisted-by: qwen3.7-plus

* 🐛 Add membership-side test for share-link token visibility

Add test coverage for the allow side of the share-link token filtering:
team members and file owners should still see all share-links, while
anonymous share-link holders only see their own token.

This protects the (:type perms) = :share-link guard from accidental
regression that could break the owner's share-link management dialog.

AI-assisted-by: qwen3.7-plus
2026-09-01 08:46:59 +02:00
Andrey Antukh 3df039abc3 Add several improvements for demo profile creation mechanism (#11257)
*  Optimize demo user setup for performance tests

Use UUID-based demo emails to prevent concurrent profile collisions.\nUse fast PBKDF2 hashing for demo profiles while keeping regular user hashing unchanged.\nAdd focused coverage for hashing, email uniqueness, and the feature flag.\n\nAI-assisted-by: gpt-5.6-luna

* 🐛 Harden font upload test setup

Report upload-session errors before chunk validation.
Skip chunk uploads when the session ID is invalid.
Remove unnecessary Mockery state from the foreign-font test.

AI-assisted-by: gpt-5.6-luna

*  Add demo profile purge task

Schedule delayed deletion for demo profiles through the worker system.
Restore normal profile filtering and cover the purge handler with tests.

AI-assisted-by: gpt-5.6-luna
2026-09-01 08:46:28 +02:00
Andrey Antukh 92c2079ae2 🐛 Add configurable limits for ZIP entry count and object size in v3 import (#11022)
* 🐛 Add configurable limits for ZIP entry count and object size in v3 import

Add binfile-import-max-zip-entries (default 500,000) and
binfile-import-max-object-size (default 100 MiB) config entries.
Both are configurable via PENPOT_BINFILE_IMPORT_MAX_ZIP_ENTRIES and
PENPOT_BINFILE_IMPORT_MAX_OBJECT_SIZE env vars.

Entry count is checked before processing begins. Per-object size is
checked after each storage object content is resolved.

AI-assisted-by: mimo-v2.5-pro

* 🐛 Enforce actual decompressed byte limits on v3 import

The previous object-size check trusted the ZIP entry header's declared
size (ZipEntry.getSize()), which a malicious zip-bomb can forge. The
check would pass, then the full decompressed payload would be read
anyway during hashing and storage persistence.

Add size-limiting-stream, a FilterInputStream wrapper that counts
actual bytes read and raises :validation :max-file-size-reached when
the configured limit is exceeded. Wire it into zip-entry-storage-content
so both the hash calculation and storage write paths are bounded by
real decompressed bytes, not declared header size.

Also wire import limits into management.clj (clone-template) and
debug.clj (import-handler + clone path) for defense-in-depth, and
add a test that exercises the object-size limit with a real storage
object in the exported ZIP.

AI-assisted-by: mimo-v2.5-pro
2026-09-01 08:44:06 +02:00
Andrey Antukh 73d3d63616 Enable a way to provide custom opencode config on starting devenv 2026-08-31 16:31:24 +02:00
Luis de Dios fc207a1768 🐛 Fix shapes flyout in toolbar only opens with the space key, and doesn't close when pressed again (#11331) 2026-08-31 14:00:14 +02:00
Pablo Alba 93ac6d8338 Add a new nitrate endpoint to check the airgapped flag (#11434) 2026-08-31 13:31:17 +02:00
Luis de Dios 806f947360 🐛 Fix avoid empty space at the bottom when there are many layers (#11427) 2026-08-31 12:38:20 +02:00
Luis de DiosandEva Marco bfb5cae9f4 🐛 Fix viewer login modal lacks spacing between SSO buttons and work email field (#11312)
* 🐛 Fix spacing between SSO buttons and email field

* ♻️ Update scrollbar on register page

* ♻️ Translate hardcoded string

---------

Co-authored-by: Eva Marco <evamarcod@gmail.com>
2026-08-31 12:17:57 +02:00
Luis de Dios 2ed0981e8a 🐛 Fix disabled numeric input with token applied is broken (#11324) 2026-08-31 11:51:42 +02:00
Luis de Dios 169e6fcbca 🐛 Fix comments not close main menu (#11347) 2026-08-31 11:50:59 +02:00
Andrey Antukh 995a5460e5 📎 Update agents and opencode on devenv 2026-08-28 21:20:17 +02:00
Andrey Antukh 4c9aeab65e 📚 Add issue/PR CLI auto-trigger to agent guide
When an issue or PR is mentioned in conversation, fetch details via gh
CLI or scripts/gh.py instead of WebFetch. Use gh issue view for issues,
gh pr view for single PRs, and scripts/gh.py prs for multiple PRs.

AI-assisted-by: glm-5.3-flash
2026-08-28 14:55:30 +00:00
María Valderrama c01306734a 🐛 Fix default team and UI labels to use Personal Projects (#11418)
* 🐛 Fix default team and UI labels to use Personal Projects

* 📎 Code review
2026-08-28 14:04:56 +02:00
Pablo Alba 10e40e62a2 Add airgapped check to nitrate (#11421) 2026-08-28 13:58:18 +02:00
Marina López f06d1df5be 🐛 Align management API with payments service (#11414) 2026-08-28 11:54:02 +02:00
María Valderrama eb0c51ed74 📚 Rename personal space label from Your Penpot to Personal Projects (#11399) 2026-08-28 10:41:25 +02:00
Pablo Alba 18ebf5a92d 📚 Add highlights file (#11406) 2026-08-28 10:40:25 +02:00
María Valderrama cdfdbf9adc 🐛 Fix labels for teams navigation and personal files section (#11398) 2026-08-28 09:49:13 +02:00
Andrey AntukhandSumit Ridhal 1baa8bb29f 🐛 Center canvas and select layer when navigating layer search results (#10422) (#11391)
When using previous/next buttons in the layer search panel, the canvas
now centers on the matched layer and selects it in the layers panel.
Previously only the internal search cursor was advanced without any
visible canvas or selection update.

Co-authored-by: Sumit Ridhal <sridhal@redhat.com>
2026-08-27 18:33:48 +02:00
Luis de Dios 11224940ab 🐛 Fix wrong selection color on inputs (#11400) 2026-08-27 18:32:22 +02:00
Andrey AntukhandLuis de Dios 43bbf756b0 🐛 Fix font change regression and activate font preview by default (#11341)
*  Materialize font-preview flag to make it permanent

* 🐛 Fix font change regression when WebGL is disabled

* 📎 Add fixes for e2e tests

* 🐛 Fix font change from legacy render

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-08-27 18:30:33 +02:00
María Valderrama 6bedd5ac76 🐛 Fix move organization modal (#11390)
* 🐛 Fix move organization modal

* 📎 Code review
2026-08-27 12:41:32 +02:00
1834 changed files with 11475 additions and 24860 deletions

No files matched your search

-41
View File
@@ -1,41 +0,0 @@
def specs: [.. | objects | select(has("tests") and has("file"))];
def dur: [.tests[].results[]?.duration // 0] | add;
specs as $s
| ($s | map(select(any(.tests[]; .status == "unexpected")))) as $failed
| ($s | map(select(any(.tests[]; .status == "flaky")))) as $flaky
| ($s | map(select(any(.tests[]; .status == "skipped")))) as $skipped
| ($s | length) as $total
| ($s | map(dur) | add // 0 | . / 1000 | floor) as $cpu
| (if ($failed | length) > 0 then "❌"
elif ($flaky | length) > 0 then "⚠️"
else "✅" end) as $icon
| "## \($icon) Integration tests\n\n"
+ "| Total | Passed | Flaky | Failed | Skipped | Test time |\n"
+ "|---|---|---|---|---|---|\n"
+ "| \($total) | \($total - ($failed|length) - ($flaky|length) - ($skipped|length)) "
+ "| \($flaky|length) | \($failed|length) | \($skipped|length) | \($cpu / 60 | floor)m |\n"
+ (if ($failed | length) > 0 then
"\n### Failed\n\n"
+ ($failed | map("- `\(.file):\(.line)` — \(.title)") | join("\n")) + "\n"
else "" end)
+ (if ($flaky | length) > 0 then
"\n### Flaky (passed on retry)\n\n"
+ ($flaky
| map({ t: "`\(.file):\(.line)` — \(.title)",
r: ([.tests[].results[]? | select(.status == "failed")] | length) })
| sort_by(-.r)
| map("- \(.t) _(\(.r) \(if .r == 1 then "retry" else "retries" end))_")
| join("\n")) + "\n"
else "" end)
+ (if $total > 0 then
"\n<details><summary>Slowest specs</summary>\n\n"
+ ($s | map({ t: "`\(.file)` — \(.title)", d: (dur / 1000 | floor) })
| sort_by(-.d) | .[0:5]
| map("- \(.t) — \(.d)s") | join("\n"))
+ "\n\n</details>\n"
else "" end)
+4 -8
View File
@@ -17,19 +17,15 @@ on:
required: true
default: 'develop'
# Literal group name: under `workflow_call`, `github.workflow` resolves to the
# caller's workflow, which put this workflow and the other reusable one called
# by the same caller into a single shared group, and left a manual dispatch of
# the same ref in a group of its own, free to race on the same artifacts.
concurrency:
group: build-bundle-${{ inputs.gh_ref }}
group: ${{ github.workflow }}-${{ inputs.gh_ref }}
cancel-in-progress: true
jobs:
# ── 1. Decide whether there is anything to build ───────────────────────
check:
name: Check current bundle
runs-on: penpot-standar-runner
runs-on: penpot-runner-01
timeout-minutes: 10
outputs:
gh_ref: ${{ steps.vars.outputs.gh_ref }}
@@ -79,7 +75,7 @@ jobs:
# ── 2. Build and upload, only when needed ──────────────────────────────
build:
name: Build and Upload Penpot Bundle
runs-on: penpot-standar-runner
runs-on: penpot-runner-01
timeout-minutes: 90
needs: check
if: needs.check.outputs.exists == 'false'
@@ -120,7 +116,7 @@ jobs:
# ── 3. Single failure notification for the whole workflow ─────────────
notify:
name: Notify failure
runs-on: penpot-standar-runner
runs-on: penpot-runner-01
timeout-minutes: 5
needs: [check, build]
if: failure()
+1 -5
View File
@@ -5,10 +5,6 @@ on:
schedule:
- cron: '16 5-20 * * 1-5'
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
@@ -23,7 +19,7 @@ jobs:
with:
gh_ref: "develop"
build-docker-admin-console:
build-admin-console-docker:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
+1 -1
View File
@@ -6,7 +6,7 @@ on:
jobs:
build-and-push:
name: Build and push DevEnv Docker image
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
steps:
- name: Set common environment variables
+5 -9
View File
@@ -16,12 +16,8 @@ on:
required: true
default: 'develop'
# Literal group name: under `workflow_call`, `github.workflow` resolves to the
# caller's workflow, which put this workflow and the other reusable one called
# by the same caller into a single shared group, and left a manual dispatch of
# the same ref in a group of its own, free to race on the same artifacts.
concurrency:
group: build-docker-${{ inputs.gh_ref }}
group: ${{ github.workflow }}-${{ inputs.gh_ref }}
cancel-in-progress: true
env:
@@ -36,7 +32,7 @@ jobs:
# ── 1. Resolve the build key and check the whole set at once ───────────
prepare:
name: Prepare
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
timeout-minutes: 15
outputs:
gh_ref: ${{ steps.vars.outputs.gh_ref }}
@@ -111,7 +107,7 @@ jobs:
# ── 2. One build per image, in parallel, only when needed ──────────────
build:
name: Build ${{ matrix.image }}
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
timeout-minutes: 60
needs: prepare
if: needs.prepare.outputs.exists == 'false'
@@ -224,7 +220,7 @@ jobs:
# the S3 marker guarantees the branch tags were already moved.
promote:
name: Promote image set
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
timeout-minutes: 10
needs: [prepare, build]
@@ -271,7 +267,7 @@ jobs:
# ── 4. Single failure notification for the whole workflow ─────────────
notify:
name: Notify failure
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
timeout-minutes: 5
needs: [prepare, build, promote]
if: failure()
+1 -5
View File
@@ -5,10 +5,6 @@ on:
schedule:
- cron: '36 5-20 * * 1-5'
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
@@ -23,7 +19,7 @@ jobs:
with:
gh_ref: "staging"
build-docker-admin-console:
build-admin-console-docker:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
+2 -18
View File
@@ -6,12 +6,6 @@ on:
tags:
- '*'
# Keyed by ref and never cancelling: pushing 2.17.2 shortly after 2.17.2-RC1
# must not abort the release already in flight.
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: false
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
@@ -26,18 +20,10 @@ jobs:
with:
gh_ref: ${{ github.ref_name }}
build-docker-admin-console:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
notify:
name: Notifications
runs-on: ubuntu-24.04
needs:
- build-docker
- build-docker-admin-console
needs: build-docker
steps:
- name: Notify Mattermost
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
@@ -51,9 +37,7 @@ jobs:
publish-final-tag:
if: ${{ !contains(github.ref_name, '-RC') && !contains(github.ref_name, '-alpha') && !contains(github.ref_name, '-beta') && contains(github.ref_name, '.') }}
needs:
- build-docker
- build-docker-admin-console
needs: build-docker
uses: ./.github/workflows/release.yml
secrets: inherit
with:
-24
View File
@@ -1,24 +0,0 @@
name: _TMP TOKENS
on:
workflow_dispatch:
schedule:
- cron: '46 5-20 * * 1-5'
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "hiru-tokens-in-libs"
build-docker:
needs: build-bundle
uses: ./.github/workflows/build-docker.yml
secrets: inherit
with:
gh_ref: "hiru-tokens-in-libs"
+1 -1
View File
@@ -34,7 +34,7 @@ permissions:
jobs:
deploy:
runs-on: penpot-standar-runner
runs-on: penpot-runner-01
steps:
- name: Checkout
uses: actions/checkout@v6
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
test-backend:
if: ${{ !github.event.pull_request.draft }}
name: "Backend Tests"
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
test-common:
if: ${{ !github.event.pull_request.draft }}
name: "Common Tests"
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
composable-test-suite:
if: ${{ !github.event.pull_request.draft }}
name: "Run composable test suite (mocked backend)"
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
test-frontend:
if: ${{ !github.event.pull_request.draft }}
name: "Frontend Tests"
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
+35 -156
View File
@@ -5,37 +5,11 @@ defaults:
shell: bash
on:
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch or ref'
type: string
required: true
default: 'develop'
shards:
description: 'Shard layout (JSON array)'
type: choice
required: true
default: '[1, 2, 3, 4]'
options:
- '[1, 2, 3, 4]'
- '[1, 2, 3, 4, 5, 6]'
- '[1, 2]'
- '[1]'
workers:
description: 'Playwright workers per shard'
type: string
required: true
default: '2'
pull_request:
paths:
- 'frontend/**'
- 'common/**'
- 'render-wasm/**'
- '.github/workflows/tests-integration.yml'
types:
- opened
@@ -51,41 +25,25 @@ on:
- 'frontend/**'
- 'common/**'
- 'render-wasm/**'
- '.github/workflows/tests-integration.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || inputs.gh_ref || github.ref }}
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build-integration:
if: ${{ !github.event.pull_request.draft }}
name: "Build Integration Bundle"
runs-on: penpot-extended-runner
timeout-minutes: 30
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
outputs:
bundle_key: ${{ steps.vars.outputs.bundle_key }}
steps:
# An empty `ref` makes checkout fall back to its default (the PR merge
# ref on pull_request, the pushed ref on push).
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ inputs.gh_ref }}
# The cache key must come from the SHA actually checked out: on a manual
# run `github.sha` points at the dispatching ref, not at `gh_ref`.
- name: Extract cache key
id: vars
run: |
echo "bundle_key=integration-bundle-$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Build Bundle
working-directory: ./frontend
@@ -95,151 +53,72 @@ jobs:
- name: Store Bundle Cache
uses: actions/cache@v5
with:
key: ${{ steps.vars.outputs.bundle_key }}
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
test-integration:
if: ${{ !github.event.pull_request.draft }}
name: "Integration Tests (${{ matrix.shard }})"
runs-on: penpot-extended-runner
timeout-minutes: ${{ github.base_ref == 'staging' && 60 || 25 }}
needs: build-integration
# TEMPORARY (release stabilization): PRs targeting `staging` run on a
# single serial shard, so new flakes cannot block the release work.
# Remove the `github.base_ref` branch below to restore full parallelism.
strategy:
fail-fast: false
matrix:
shard: ${{ fromJSON(inputs.shards || (github.base_ref == 'staging' && '[1]' || '[1, 2, 3, 4]')) }}
name: "Integration Tests"
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
- /var/cache/github-runner/ms-playwright:/ms-playwright
env:
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
needs: build-integration
steps:
- name: Checkout Repository
uses: actions/checkout@v6
with:
ref: ${{ inputs.gh_ref }}
- name: Restore Cache
uses: actions/cache/restore@v5
with:
key: ${{ needs.build-integration.outputs.bundle_key }}
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
- name: Install deps
working-directory: ./frontend
run: |
corepack enable;
corepack install;
pnpm install --frozen-lockfile;
# No-op once the shared volume is warm; keeps the first run working.
- name: Install Playwright Chromium
working-directory: ./frontend
run: pnpm exec playwright install chromium
# `strategy.job-total` is the matrix size, so the shard denominator
# follows the `shards` input without being hardcoded.
- name: Run Tests
working-directory: ./frontend
env:
WORKERS: ${{ inputs.workers }}
BASE_REF: ${{ github.base_ref }}
PLAYWRIGHT_REPORTER: list,json
PLAYWRIGHT_JSON_OUTPUT_NAME: report.json
run: |
# TEMPORARY (release stabilization): see the note on the matrix above.
if [ -z "$WORKERS" ]; then
if [ "$BASE_REF" = "staging" ]; then WORKERS=1; else WORKERS=2; fi
fi
echo "Running shard ${{ matrix.shard }}/${{ strategy.job-total }} with $WORKERS workers"
pnpm exec playwright test --project default \
--workers="$WORKERS" \
--shard=${{ matrix.shard }}/${{ strategy.job-total }} \
--reporter=blob
./scripts/test-e2e
- name: Upload blob report
- name: Flaky summary
if: always()
working-directory: ./frontend
run: |
if [ ! -f report.json ]; then
echo "No report.json produced (the run failed early)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
jq -r '
[ .. | objects
| select(has("tests") and has("file"))
| select(any(.tests[]; .status == "flaky"))
| "- `\(.file):\(.line)` — \(.title)"
] as $f
| "## Flaky tests: \($f | length)\n"
+ (if ($f | length) == 0 then "_none_" else ($f | join("\n")) end)
' report.json >> "$GITHUB_STEP_SUMMARY"
- name: Upload JSON report
uses: actions/upload-artifact@v7
if: always()
with:
name: integration-blob-report-${{ matrix.shard }}
path: frontend/blob-report/
name: integration-json-report
path: frontend/report.json
overwrite: true
retention-days: 3
retention-days: 30
- name: Upload test result
uses: actions/upload-artifact@v7
if: always()
with:
name: integration-tests-result-${{ matrix.shard }}
name: integration-tests-result
path: frontend/test-results/
overwrite: true
if-no-files-found: ignore
retention-days: 3
merge-reports:
if: ${{ always() && !github.event.pull_request.draft && needs.test-integration.result != 'skipped' }}
name: "Merge Integration Reports"
runs-on: penpot-extended-runner
timeout-minutes: 15
needs: test-integration
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- name: Checkout Repository
uses: actions/checkout@v6
with:
ref: ${{ inputs.gh_ref }}
- name: Install deps
working-directory: ./frontend
run: |
corepack enable;
corepack install;
pnpm install --frozen-lockfile;
- name: Download blob reports
uses: actions/download-artifact@v7
with:
path: frontend/all-blob-reports
pattern: integration-blob-report-*
merge-multiple: true
- name: Merge into HTML report
working-directory: ./frontend
env:
PLAYWRIGHT_JSON_OUTPUT_NAME: report.json
run: |
pnpm exec playwright merge-reports \
--reporter=html,json,list ./all-blob-reports
- name: Test summary
if: always()
working-directory: ./frontend
run: |
if [ ! -f report.json ]; then
echo "No report produced (all shards failed early)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
jq -r -f ../.github/scripts/playwright-summary.jq report.json >> "$GITHUB_STEP_SUMMARY"
- name: Upload HTML report
uses: actions/upload-artifact@v7
with:
name: integration-html-report
path: frontend/playwright-report/
overwrite: true
retention-days: 7
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
test-library:
if: ${{ !github.event.pull_request.draft }}
name: "Library Tests"
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
test-mcp:
if: ${{ !github.event.pull_request.draft }}
name: "Test MCP"
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
+2 -2
View File
@@ -53,7 +53,7 @@ jobs:
api-test-suite-mocked:
if: ${{ github.event_name != 'workflow_dispatch' && !github.event.pull_request.draft }}
name: "Run Plugin API Test Suite (mocked)"
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
@@ -95,7 +95,7 @@ jobs:
# api-test-suite-live:
# if: ${{ github.event_name == 'workflow_dispatch' }}
# name: Run Plugin API Test Suite (live)
# runs-on: penpot-extended-runner
# runs-on: penpot-runner-02
# container:
# image: penpotapp/devenv:latest
#
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
test-plugins:
if: ${{ !github.event.pull_request.draft }}
name: Plugins Runtime Linter & Tests
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
test-render-wasm:
if: ${{ !github.event.pull_request.draft }}
name: "Render WASM Tests"
runs-on: penpot-extended-runner
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
+1 -2
View File
@@ -24,6 +24,7 @@ opencode.json
!AGENTS.md
!CODE_OF_CONDUCT.md
!SECURITY.md
!HIGHLIGHTS.md
/*.png
/*.svg
/*.sql
@@ -58,8 +59,6 @@ opencode.json
/docker/images/bundle*
/exporter/target
/exporter/.shadow-cljs
/exporter/resources/wasm/
/exporter/src/app/wasm/shared.js
/frontend/.storybook/preview-body.html
/frontend/.storybook/preview-head.html
/frontend/playwright-report/
+1 -4
View File
@@ -3,11 +3,8 @@ description: Execute a ready plan end-to-end — create a GitHub issue, branch i
agent: build
---
# Implement Plan
This command is run once a plan is ready (for example, from plan mode). Execute
the plan already prepared in the current session context — it does not take
extra arguments. Follow these steps in order.
the plan already prepared in the current session context. Follow these steps in order.
## 1. Create the issue
-25
View File
@@ -1,25 +0,0 @@
Act as a senior software engineer and perform a thorough review.
## Instructions
1. **Determine what is being reviewed** from the provided context:
- **If it is a plan** (implementation plan, design document, task breakdown) → load the **`plan-review`** skill.
- **If it is code** (diff, PR, code change) → load the **`code-review`** skill.
2. Read `AGENTS.md` and follow its instructions for finding and reading all related testing documentation from memories before reviewing.
3. **Skip generated files, lockfile-only changes, and unrelated modifications** unless they introduce security risks.
4. Follow the loaded skill's process and produce its output format.
## Strong Rules
1. Do not invent problems. Every finding must be real and actionable.
2. Do not modify any code and do not create a commit — this command only reviews.
3. Be specific and constructive. "This could be better" is not helpful — explain why and how.
4. Prioritize by impact. One structural issue outweighs ten nits.
5. Missing tests are an issue, not a suggestion. Report as a severity-tagged finding — never as a recommendation.
## Context
$ARGUMENTS
+8 -16
View File
@@ -106,6 +106,8 @@ For detailed security guidance, see `security-and-hardening`.
| **Low:** | Minor, optional | Author may ignore — formatting, style preferences |
| **Suggestion:** | Worth considering | Not required, but improves the code |
**Unique finding IDs.** Assign every finding a stable identifier: `F1`, `F2`, `F3`, … numbered in order of severity (Critical first, then High, Medium, Low, Suggestion). Use the ID everywhere the finding is mentioned — in section headers, in the verdict, in follow-up discussion. Never renumber within a review. Example: `**F3 (High)** — `app/validate.cljs:42` — duplicate branch logic…`.
For each finding, describe the circumstances under which it could fail: specific inputs, load conditions, timing, or user actions that trigger the problem. "This crashes when input is null" is actionable; "this might crash" is not.
Lead with what matters: correctness and security first, then structural issues, then everything else. A few high-conviction comments beat a long list.
@@ -122,11 +124,11 @@ Briefly explain what the code does and give an overall assessment.
### Critical and High-Priority Issues
List problems that could cause security incidents, data loss, crashes, incorrect behavior, or major performance degradation. For each: state the severity, identify the file/function/code section, explain why it's a problem, describe failure circumstances, and provide a concrete improvement with corrected code when useful.
List problems that could cause security incidents, data loss, crashes, incorrect behavior, or major performance degradation. Each finding gets its unique ID (`F1`, `F2`, …). For each: state the severity, identify the file/function/code section, explain why it's a problem, describe failure circumstances, and provide a concrete improvement with corrected code when useful.
### Other Findings
List medium- and low-priority issues, including maintainability and design concerns.
List medium- and low-priority issues, including maintainability and design concerns. Continue the ID sequence started above (`F3`, `F4`, …).
### Suggested Refactoring
@@ -148,6 +150,8 @@ Choose one:
- **Approve with minor changes** — Good to merge after addressing low/medium issues
- **Request changes** — Critical or high issues must be resolved before merge
List the finding IDs the verdict depends on (e.g. "Request changes: F1, F4").
## Change Sizing
Small, focused changes are easier to review, faster to merge, and safer to deploy.
@@ -231,25 +235,13 @@ For supply-chain risk triage, follow the `security-and-hardening` skill.
## Verification
After review is complete:
Before emitting the verdict, verify the change as it stands. This is the reviewer's own due diligence — it covers the state of the code at review time, not the later resolution of findings (fixing findings is the author's job; confirming them is a new review):
- [ ] All Critical issues are resolved
- [ ] All Required (no-prefix) changes are resolved or explicitly deferred with justification
- [ ] Tests pass
- [ ] Tests pass — run them yourself, don't trust the claim
- [ ] Build succeeds
- [ ] The verification story is documented (what changed, how it was verified)
- [ ] Dependency upgrades reviewed against changelog, isolated per package, verified by green suite
## Multi-Model Review Pattern
Use different models for different review perspectives:
```
Model A writes the code → Model B reviews → Model A addresses feedback → Human makes the final call
```
Different models have different blind spots.
## See Also
- For detailed security review guidance, see `security-and-hardening`
+172 -66
View File
@@ -1,13 +1,13 @@
---
name: planner
description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan (Context, Affected modules, Approach, Risks, Testing). Always output to the user; additionally save to .opencode/plans/YYYY-MM-DD-<title>.md.
description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan with task breakdown, acceptance criteria, sizing, and checkpoints. Always output to the user and save to .opencode/plans/YYYY-MM-DD-<title>.md.
---
# Planner
Read-only senior software architect role for Penpot. Produces structured
implementation plans that engineers or other agents can execute. Never writes
or modifies code.
implementation plans with task breakdowns that engineers or other agents can
execute. Never writes or modifies code.
## When to Use
@@ -18,24 +18,29 @@ or modifies code.
- The user asks "how would I implement X?" or "what's involved in fixing Y?".
- The user is about to start non-trivial work and wants a bite-sized task
breakdown.
- A task feels too large or vague to start.
- Work needs to be parallelized across multiple agents or sessions.
Do **not** use this skill to actually implement anything — it is read-only.
**When NOT to use:** Single-file changes with obvious scope, or when the spec
already contains well-defined tasks.
## Role
You are a Senior Software Architect working on Penpot, an open-source design
tool. Your sole responsibility is planning and analysis — you do NOT write or
modify code.
You help users understand the Penpot codebase, design solutions, and produce
implementation plans that other agents or developers can execute. The plan
tells them what to build and how to verify it, task by task.
You help users understand the codebase, design solutions, and create detailed
implementation plans that other agents or developers can execute. Document
everything they need to know: which files to touch for each task, code patterns,
tests, and how to verify correctness. Apply DRY and KISS principles.
The implementer reads the project's agent docs (`AGENTS.md`, project memories
such as `mem:critical-info`, `mem:testing`, and each module's core memory)
before working. Reference those memories instead of re-explaining tooling,
conventions, or test design — explain in the plan only what they do not cover.
Do **not** suggest commit messages or commit names anywhere in your plans or
responses — committing is the developer's responsibility.
responses — committing is the implementer's responsibility.
## Required Reading Before Planning
## CRITICAL: Required Reading Before Planning
Before drafting any plan, work through the project's own guidance:
@@ -50,6 +55,8 @@ Before drafting any plan, work through the project's own guidance:
Skipping this step is the #1 cause of incorrect or incomplete plans.
---
## The Planning Process
### Phase 1: Architecture Analysis
@@ -64,16 +71,42 @@ Skipping this step is the #1 cause of incorrect or incomplete plans.
### Phase 2: Task Breakdown
Implementation order follows the monorepo's dependency graph:
`frontend -> common`, `backend -> common`, `exporter -> common`,
`frontend -> render-wasm`. Build shared foundations first, then layer
consumers on top.
#### Identify the Dependency Graph
Map what depends on what, following the monorepo's module dependency graph:
```
common (shared types, schemas — no deps)
├── backend (depends common)
│ ├── RPC handlers
│ └── persistence / migrations
├── frontend (depends common, render-wasm)
│ ├── UI components
│ └── state / API integration
├── exporter (depends common)
└── render-wasm (consumed by frontend)
```
Implementation order follows the dependency graph bottom-up: build shared
foundations first, then layer consumers on top.
#### Slice Vertically
Instead of building all of common, then all of backend, then all of frontend —
build one complete feature path at a time:
**Bad (horizontal slicing):**
```
Task 1: Build all common types
Task 2: Build all backend handlers
Task 3: Build all frontend components
```
**Good (vertical slicing):**
```
Task 1: common data types + schema ← foundation
Task 2: backend RPC handler + persistence
@@ -89,39 +122,58 @@ Each task follows this structure:
```markdown
## Task [N]: [Short descriptive title]
**Description:** One paragraph explaining what this task accomplishes.
**Description:** One or two paragraphs explaining what this task accomplishes.
Should be clear and concise.
**Rationale:** Why this task exists and why this approach over the obvious
alternatives — design decisions, trade-offs, constraints discovered during
analysis. One or two sentences; skip only if genuinely trivial.
**Code sketch (optional):** Signature-, type-, or shape-level example when the
intended interface is non-obvious. Keep it short — a skeleton that fixes the
contract (function signature, model fields, error shape), never a full
implementation. Omit when the task is mechanical.
**Acceptance criteria:**
- [ ] [Specific, testable condition]
- [ ] [Specific, testable condition]
**Verification:**
- [ ] Tests pass (module-specific test command)
- [ ] Lint/formatter passes (module-specific check command)
- [ ] Relevant tests pass (module-specific test command).
- [ ] Lint/formatter passes (module-specific check command), if applicable.
- [ ] The core flow works end-to-end, if applicable.
**Dependencies:** [Task numbers this depends on, or "None"]
**Files likely touched:**
- `path/to/file.clj`
- `path/to/file_test.clj`
**Estimated scope:** [XS: 1 file | S: 1-2 files | M: 3-5 files | L: 5+ files]
```
Replace "module-specific test command" with the actual commands for the module
(e.g. `clojure -M:dev:test` for backend/common, `npx shadow-cljs compile test && npx karma start` for frontend,
or the commands noted in the module's core memory).
(e.g. `clojure -M:dev:test` for backend/common,
`npx shadow-cljs compile test && npx karma start` for frontend, or the
commands noted in the module's core memory).
When possible, design each task with TDD in mind: acceptance criteria double
as a test list, and the natural first step of the task is writing those tests
before the implementation. Some tasks resist this (config, migrations, pure
wiring) — for those, keep the usual verification steps.
#### Estimate Scope
| Size | Files | Scope |
|------|-------|-------|
| **XS** | 1 | Single function, config change, or schema tweak |
| **S** | 1-2 | One handler or component method |
| **M** | 3-5 | One vertical feature slice |
| **L** | 5-8 | Multi-component feature |
| **XL** | 8+ | **Too large — break it down further** |
| Size | Files | Scope | Example |
|------|-------|-------|---------|
| **XS** | 1 | Single function, config change, or schema tweak | Add a validation rule |
| **S** | 1-2 | One handler or component method | Add a new RPC endpoint |
| **M** | 3-5 | One vertical feature slice | Bookmark CRUD with tests |
| **L** | 5-8 | Multi-component feature | Search with filtering and pagination |
| **XL** | 8+ | **Too large — break it down further** | — |
If a task is L or larger, break it into smaller tasks. Agents perform best on
S and M tasks.
If a task is XL, it should be broken into smaller tasks. Agents perform best
on S and M tasks.
**When to break a task down further:**
- It would take more than one focused session
@@ -141,11 +193,11 @@ Arrange tasks so that:
Add explicit checkpoints with the relevant module commands:
```markdown
## Checkpoint: After Tasks 1-3
- [ ] All tests pass (module-specific command)
- [ ] Lint/format passes (module-specific command)
- [ ] Core flow works end-to-end
- [ ] Review with human before proceeding
### Checkpoint: After Tasks 1-3
- [ ] Relevant tests pass (module-specific command).
- [ ] The relevant build or compilation passes, if applicable.
- [ ] The core flow works end-to-end.
- [ ] Review with human before proceeding.
```
## Requirements
@@ -159,7 +211,7 @@ Add explicit checkpoints with the relevant module commands:
- Apply DRY and KISS principles to the proposed implementation.
- Define a testing strategy aligned with each affected module's tooling.
- Every task must have acceptance criteria and verification steps.
- Checkpoints must exist between major phases.
- Checkpoints must exist after every 2-3 tasks.
## Constraints
@@ -168,7 +220,8 @@ Add explicit checkpoints with the relevant module commands:
`.opencode/plans/`.
- You do **not** run builds, tests, linters, or any commands that modify state.
- You do **not** create git commits or interact with version control.
- You do **not** execute shell commands beyond read-only searches.
- You do **not** execute shell commands beyond read-only searches (`rg`, `ls`,
`find`, `cat`, `bat`).
- Your output is a structured plan or analysis, ready for handoff to an
engineer agent or developer.
@@ -188,8 +241,9 @@ slug is lowercase, hyphen-separated, and a short summary of the task
(e.g. `add-batch-get-profiles-for-file-comments`). Create the
`.opencode/plans/` directory if it does not exist.
Always attempt the write. If the user explicitly provides a target file path,
use that path instead of the default.
IMPORTANT: The plan agent has write permission specifically for
`.opencode/plans/` — always attempt the write. If the user explicitly provides
a target file path, use that path instead of the default.
### Plan Document Template
@@ -212,41 +266,75 @@ use that path instead of the default.
security implications.]
## Approach
[Step-by-step implementation plan with file paths, function names, and code
shape where applicable. Group steps into atomic, ordered tasks.]
[A short strategy summary: 3-5 sentences describing the overall approach and
the shape of the dependency graph (what depends on what, what gets built
first). High-level only — the task-by-task detail lives in the Task List.]
## Task List
### Phase 1: Foundation
- [ ] Task 1: ...
- [ ] Task 2: ...
Each task uses the full task structure defined in
[Write Tasks](#write-tasks) — description, rationale, acceptance criteria,
verification, dependencies, files, estimated scope, and optional code sketch.
Never reduce a task to a one-line checkbox; the plan must be self-contained
and executable without other context.
### Checkpoint: Phase 1
- [ ] Tests pass, lint/formatter clean (module-specific commands)
Tasks are a flat, ordered list — a plan is not a roadmap. Do not group tasks
into phases, milestones, or sprints; ordering and dependencies are already
captured per task. Insert a checkpoint after every 2-3 tasks.
### Phase 2: Core Features
- [ ] Task 3: ...
- [ ] Task 4: ...
## Task 1: [Short descriptive title]
### Checkpoint: Phase 2
- [ ] End-to-end flow works
**Description:** [What this task accomplishes.]
### Phase 3: Polish
- [ ] Task 5: ...
- [ ] Task 6: ...
**Rationale:** [Why this approach over the alternatives.]
### Checkpoint: Complete
- [ ] All acceptance criteria met
- [ ] Ready for review
**Acceptance criteria:**
- [ ] [Specific, testable condition]
## Testing Strategy
[How to verify: which test commands to run per module, what cases to cover,
manual verification steps, lint/format checks. Consult each module's core
memory for the exact commands.]
**Verification:**
- [ ] Relevant tests pass (module-specific command).
**Dependencies:** None
**Files likely touched:**
- `path/to/file`
**Estimated scope:** [XS: 1 file | S: 1-2 files | M: 3-5 files | L: 5+ files]
**Code sketch (optional):** [Short contract-level example, only if the shape
is non-obvious.]
## Task 2: [Short descriptive title]
[Same structure as Task 1.]
## Task 3: [Short descriptive title]
[Same structure as Task 1.]
### Checkpoint: After Tasks 1-3
- [ ] Relevant tests pass (module-specific command).
- [ ] The relevant build or compilation passes, if applicable.
- [ ] The core flow works end-to-end.
- [ ] Review with human before proceeding.
## Task 4: [Short descriptive title]
[Same structure as Task 1.]
## Task 5: [Short descriptive title]
[Same structure as Task 1.]
## Verification & Testing
[How to verify each task and the whole plan: the project's real test, lint,
build, and run commands (extracted during Required Reading), coverage
expectations, and manual checks. Consult each module's core memory for the
exact commands.]
## Parallelization Opportunities
- **Safe to parallelize:** Independent feature slices across separate
modules, tests for already-implemented features
modules, tests for already-implemented features, documentation
- **Must be sequential:** Shared common schema changes, database migrations
- **Needs coordination:** Features that share a contract (define the contract
first, then parallelize)
@@ -259,13 +347,31 @@ When the plan is purely analytical (e.g. a code review or feasibility study
with no implementation), skip the **Approach** and **Task List** sections and
lead with **Findings** instead, keeping the rest of the structure.
## Common Rationalizations
| Rationalization | Reality |
|---|---|
| "I'll figure it out as I go" | That's how you end up with a tangled mess and rework. 10 minutes of planning saves hours. |
| "The tasks are obvious" | Write them down anyway. Explicit tasks surface hidden dependencies and forgotten edge cases. |
| "Planning is overhead" | Planning is the task. Implementation without a plan is just typing. |
| "I can hold it all in my head" | Context windows are finite. Written plans survive session boundaries and compaction. |
## Red Flags
- Delivering prose without a task breakdown
- Tasks that say "implement the feature" without acceptance criteria
- No verification steps in the plan
- All tasks are XL-sized
- No checkpoints between tasks
- Dependency order isn't considered
## Verification Checklist
Before starting implementation, confirm:
Before delivering the plan, confirm:
- [ ] Every task has acceptance criteria
- [ ] Every task has a verification step
- [ ] Task dependencies are identified and ordered correctly
- [ ] No task touches more than ~5 files
- [ ] Checkpoints exist between major phases
- [ ] The human has reviewed and approved the plan
- [ ] No task is XL or larger — break it down instead
- [ ] Checkpoints exist after every 2-3 tasks
- [ ] The plan is ready for human review
+24 -8
View File
@@ -477,9 +477,15 @@ There are exactly two types:
release, but the PR is being released elsewhere — the fix may not
actually ship here.
2. **PR is in the milestone, but the issue it closes is in a different
milestone (or has no milestone).** The PR is being released here, but
the issue it fixes is being released in a different version (or never
tracked in a milestone) — the changelog pairing is misleading.
milestone.** The PR is being released here, but the issue it fixes is
being released in a different version — the changelog pairing is
misleading.
**Exception — issue with no milestone is NOT an anomaly.** Milestones
are only required for issues tracked in the "Main" project. A milestone
PR that closes an issue with no milestone references an issue from
another (probably private) project; that is expected and the issue is
not part of this changelog. Do not report it.
**Anything else is not an anomaly.** Other discrepancies (exclusion
labels on in-changelog issues, missing valid issues, unmerged PR
@@ -635,6 +641,10 @@ for pr_num in sorted(changelog_prs):
if get_pr_milestone(pr_num) != MILESTONE: continue
for issue_num in pr.get('closing_issues', []):
issue_ms = get_issue_milestone(issue_num)
# No milestone = issue from another (probably private) project —
# milestones are only required for the "Main" project. Not an
# anomaly, and the issue never belongs in this changelog.
if issue_ms is None: continue
if issue_ms != MILESTONE:
anomalies_b.append({
'pr': pr_num,
@@ -657,7 +667,7 @@ with open(OUTPUT, 'w') as f:
f.write('## Summary\n\n')
f.write(f'- **Issue in {MILESTONE}, referenced PR in different milestone or no milestone:** {n_a}\n')
f.write(f'- **PR in {MILESTONE}, closing issue in different milestone or no milestone:** {n_b}\n')
f.write(f'- **PR in {MILESTONE}, closing issue in a different milestone:** {n_b}\n')
f.write(f'- **Total anomalies:** {n_a + n_b}\n\n')
# --- Anomalies section ---
@@ -686,7 +696,7 @@ with open(OUTPUT, 'w') as f:
f.write('\n')
if n_b:
f.write(f'\n### PR in {MILESTONE}, closing issue in different milestone or no milestone\n\n')
f.write(f'\n### PR in {MILESTONE}, closing issue in a different milestone\n\n')
by_pr = {}
for b in anomalies_b:
by_pr.setdefault(b['pr'], []).append(b)
@@ -721,8 +731,11 @@ milestone mismatches between issues and their referenced PRs:
1. **Issue in milestone, referenced PR in different milestone or no milestone**
the changelog claims a fix here, but the PR is released elsewhere.
2. **PR in milestone, closing issue in different milestone or no milestone**
2. **PR in milestone, closing issue in a different milestone**
the PR is released here, but the issue it fixes belongs to another version.
(An issue with *no* milestone belongs to another, probably private,
project — milestones are only required on the "Main" project — so it is
neither an anomaly nor a changelog candidate.)
**Rule violations are not in the report** — they are workflow errors the
LLM must fix directly in `CHANGES.md` during step 6a (pre-flight checks).
@@ -799,8 +812,11 @@ self-contained and clickable in any Markdown viewer.
- **Anomaly = milestone mismatch only.** The report contains only milestone
mismatches: (1) the issue is in this milestone but the referenced PR is
in a different milestone (or unassigned), and (2) the PR is in this
milestone but the issue it closes is in a different milestone (or
unassigned). These are anomalies because the changelog pairing is
milestone but the issue it closes is in a different milestone. An
*unassigned* (milestone-less) issue closed by a milestone PR is **not**
an anomaly: milestones are required only for the "Main" project, so such
issues come from another (probably private) project and are not changelog
candidates. These anomalies are reported because the changelog pairing is
*misleading* — the human needs to decide whether the milestone or the
changelog is wrong. All other discrepancies (exclusion labels, missing
valid issues, unmerged PR references, duplicates, stale milestone
-36
View File
@@ -30,46 +30,10 @@
- `objects-gc` removes deleted domain rows and touches their storage object IDs.
- Use `::db/reuse-conn true` with `sto/resolve` inside a database transaction.
## Connection Reuse Details
### `app.storage/resolve` patterns:
**1. Pool mode (default)** - `(sto/resolve cfg)`
- Returns storage abstraction from config
- Uses whatever database pool is available
- **Safe to call outside transaction context**
- Used in: `rpc/commands/media.clj:363`, `rpc/commands/auth.clj:327`, `rpc/commands/profile.clj:362`
**2. Connection reuse mode** - `(sto/resolve cfg ::db/reuse-conn true)`
- Internally calls `db/get-connection cfg` to obtain connectable
- Configures storage with the specific connection from config
- **Must be paired with transaction that owns this connection**
- Used in: `features/fdata.clj:100`, `rpc/commands/media.clj:425`, `rpc/commands/files_thumbnails.clj:307,319`, `binfile/v3.clj:722`
**3. Explicit configuration** - `(sto/configure storage conn)`
- Sets `::db/conn` on storage map directly
- Asserts `db/conn? connection` (storage.clj:349)
- Used inside `db/tx-run!` blocks where `conn` is already available
- Used in: `tasks/file_gc.clj:256`, `rpc/commands/files_thumbnails.clj:347,371`
### Key Warning (from function notes):
The improved note in `import-storage-objects` and `handle-persistence` warns:
**Do not reuse the main database connection for storage operations within a transaction.** The storage upload process can fail mid-operation, leaving orphaned objects on the backend. If the outer transaction aborts, pending storage objects become unreconciliable because the storage subsystem registers its pending state in separate transactions.
### Rule of Thumb for `sto/put-object!`:
Since `put-object!` uses backend-specific operations (`impl/resolve-backend` + `impl/put-object`) and does not directly use `::db/conn` or `::db/pool`, **all usage of `put-object!` will never run inside a common transaction** (if configured at all). The storage backend operations are independent of the database transaction boundary.
## Deduplication
- Deduplication requires `::sto/deduplicate?`, a content hash, and bucket metadata.
- The lookup matches hash, bucket, backend, and `deleted_at IS NULL`.
- The lookup only considers rows with `status='valid'`; pending rows are invisible.
- A hit whose blob is missing is repaired in place: the same row/id is kept,
and `put-object!` rewrites the blob under that id. This heals all existing
references to the object. If the rewrite fails, the row is left live and
valid for a later retry.
- The lookup does not include file ID, profile ID, team ID, or organization ID.
- Objects can therefore share content across users and files within one bucket.
- Deleted objects are not reused.
+2 -1
View File
@@ -6,6 +6,7 @@ Compose-based dev environment under `docker/devenv/`, driven by `manage.sh`. Par
- `penpotdev-infra`: shared `postgres`, `minio`, `minio-setup`, `mailer`, `ldap`. File: `docker-compose.infra.yml`.
- `penpotdev-wsN` (N=0,1,…): per-instance `main` + `redis` (Valkey). File: `docker-compose.main.yml`. ws0 (a.k.a. `main`) binds `$PWD`; ws1+ bind clones at `${PENPOT_WORKSPACES_DIR}/wsN/` (default `~/.penpot/penpot_workspaces/`), maintained by the developer.
- Optional overlay `docker-compose.opencode.yml`: added by `instance-compose` as an extra `-f` only when `PENPOT_OPENCODE_CONFIG_DIR` is set (i.e. `run-devenv --opencode-config-dir DIR` ran in this process). Bind-mounts the host dir at `/home/penpot/.config/opencode` (`:z`). Flag-only, per-call; not read from ambient env. Parser `parse-opencode-config-dir` absolutizes (`~`, realpath) because compose resolves relative bind sources against the compose file's dir. Only instances brought up with the flag get the mount.
- All projects join external network `penpot_shared`. Created idempotently by `ensure-devenv-network`, never removed by lifecycle commands.
## Source-of-truth files
@@ -65,7 +66,7 @@ No `--delete` on the working-tree pass: gitignored caches in the workspace survi
## CLI surface
- `run-devenv --agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet).
- `run-devenv --agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX] [--opencode-config-dir DIR]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet). `--opencode-config-dir DIR` bind-mounts DIR at `~/.config/opencode` in-container via the optional overlay above; mount applies at container creation, so changing it requires stop + re-run.
- `stop-devenv [--ws main|0|wsN|N] [--all]`: stop instances. Flags mutually exclusive. `--ws N` stops just that workspace. `--ws 0` or no flag stops ws0; shared infra shuts down only if no other instances remain. `--all` stops every ws highest-first then ws0, then infra.
- `run-devenv`: legacy alias, ws0 non-agentic attached.
- `attach-devenv [--ws main|0|wsN|N]`: pure attach. Fails fast if instance/session missing.
@@ -17,18 +17,9 @@
## Tile/render behavior
- Raster `Fill::Image`: skip `save_layer` unless the shape has an image filter; plain
Rect/Frame (no corners) also skip the container clip (`draw_image_fill` in fills.rs).
- Zoom settle: visible tiles present via `FrameType::ViewportReady` before interest-ring
work; crop-cache rebuild is deferred to the later `Full` so the soft→sharp snap is
compose+present only.
- Interactive transforms are distinct from viewport fast mode. `set_modifiers_start` enables fast mode and interactive transform; interactive transform still flushes each animation frame.
- During interactive transform, modifier tile invalidation is deferred to `render()` once per rAF. Outside interactive transform, `set_modifiers` rebuilds modifier tiles immediately.
- `set_modifiers_end` disables fast/interactive state and cancels pending async render; the caller must request the final full-quality render.
- Plain viewport fast mode (`options.is_viewport_interaction()`) renders from cache and does not flush target output inside `process_animation_frame`; interactive transforms do flush.
- Zoom settle wipes the tile texture cache in `set_view_end`. Mid-zoom overlays
key tiles by scale; shape edits must `invalidate_cached_tiles_intersecting`
the oldnew extrect so those overlays do not keep pre-edit pixels.
- Pending tile priority is intentionally reversed by pop order; check the queue construction before changing tile scheduling.
- Frames with a fill may use `render_frame_container_drop_shadow` (direct rrect +
blur saveLayer on `DropShadows`) when `uses_direct_container_drop_shadow` is true.
- Zoom changes rebuild the tile index while preserving cached tile textures. Avoid replacing that path with shallow rebuilds if blur/shadow cache preservation matters.
- Pending tile priority is intentionally reversed by pop order; check the queue construction before changing tile scheduling.
+18 -1
View File
@@ -40,10 +40,27 @@ Skipping this step is the #1 cause of incorrect or incomplete work.
`github.com/penpot/penpot/security/advisories/GHSA-*`, extract the GHSA ID
from the URL and run `python3 scripts/gh.py advisories <GHSA-ID>` to fetch
full advisory details before proceeding.
- **Issue or PR mentioned** — When the user mentions a penpot/penpot issue or
PR (URL like `github.com/penpot/penpot/issues/<n>` / `.../pull/<n>`, or a
bare `#<n>` when context clearly refers to this repo), fetch details via CLI
instead of WebFetch:
- Issue → `gh issue view <n> --repo penpot/penpot` (add `--comments` when
discussion context matters).
- Single PR → `gh pr view <n> --repo penpot/penpot`.
- Multiple PRs (list, file, or milestone) → `python3 scripts/gh.py prs ...`.
Do this before proceeding. Only use WebFetch if the CLI fails.
## Writing Rules
Use the `ste` skill when the user explicitly requests STE, `/ste`, or ASD-STE100.
Writing rules, from Orwell, 1946. These govern prose: docs, PR text, messages. Never touch code or technical terms; swap in everyday words only where precision survives.
1. Never use a metaphor, simile or other figure of speech which you are used to seeing in print.
2. Never use a long word where a short one will do.
3. If it is possible to cut a word out, always cut it out.
4. Never use the passive where you can use the active.
5. Never use a foreign phrase, a scientific word or a jargon word if you can think of an everyday English equivalent.
6. Break any of these rules sooner than say anything outright barbarous.
Review every prose output against these rules before delivering.
---
+107 -4
View File
@@ -11,18 +11,121 @@
- Fix plugin API addTheme calls failing with the signature shown in the high-level overview [#10074](https://github.com/penpot/penpot/issues/10074) (PR: [#10359](https://github.com/penpot/penpot/pull/10359))
- Fix empty text shape not being deleted on editor exit [#10540](https://github.com/penpot/penpot/issues/10540) (PR: [#10541](https://github.com/penpot/penpot/pull/10541))
- Fix broken token pills showing wrong default state when not selected [#10524](https://github.com/penpot/penpot/issues/10524) (PR: [#10535](https://github.com/penpot/penpot/pull/10535))
- Replace hyphens with bullets in subscription benefits list [#10547](https://github.com/penpot/penpot/issues/10547) (PR: [#10523](https://github.com/penpot/penpot/pull/10523))
- Fix Chinese (zh-CN) translation showing wrong label for Intersection in board path menu (by @sawirricardo) [#10346](https://github.com/penpot/penpot/issues/10346) (PR: [#10381](https://github.com/penpot/penpot/pull/10381))
- Fix invalid formulas being accepted in numeric inputs (by @AKnassa) [#9581](https://github.com/penpot/penpot/issues/9581) (PR: [#10659](https://github.com/penpot/penpot/pull/10659))
- Fix radial gradient handles blowing up in size when rotated on ellipses (by @AKnassa) [#10069](https://github.com/penpot/penpot/issues/10069) (PR: [#10666](https://github.com/penpot/penpot/pull/10666))
- Fix plugin API validation errors being too generic to diagnose the failure (by @AKnassa) [#10072](https://github.com/penpot/penpot/issues/10072) (PR: [#10667](https://github.com/penpot/penpot/pull/10667))
- Fix crash with referential integrity error when deleting a component inside a grid (by @Alotor) [#10101](https://github.com/penpot/penpot/issues/10101) (PR: [#10956](https://github.com/penpot/penpot/pull/10956))
- Fix component copies not preserving rotation when the main component has changes [#10109](https://github.com/penpot/penpot/issues/10109) (PR: [#10574](https://github.com/penpot/penpot/pull/10574))
- Fix text width and height staying stale after setting growType in the plugin API [#10207](https://github.com/penpot/penpot/issues/10207) (PR: [#9898](https://github.com/penpot/penpot/pull/9898))
- Fix padding not painted until expanding the 4-sides padding option [#10278](https://github.com/penpot/penpot/issues/10278) (PR: [#10602](https://github.com/penpot/penpot/pull/10602))
- Fix files with custom fonts breaking with a referential integrity error when moved between teams (by @filipsajdak) [#10496](https://github.com/penpot/penpot/issues/10496) (PR: [#10837](https://github.com/penpot/penpot/pull/10837))
- Fix clicking overlapping comment bubbles zooming to 20000% without showing the comments [#10526](https://github.com/penpot/penpot/issues/10526) (PR: [#10543](https://github.com/penpot/penpot/pull/10543))
- Fix user menu subsections in the dashboard not closing when hovering away from the parent option (by @AKnassa) [#10549](https://github.com/penpot/penpot/issues/10549) (PR: [#10639](https://github.com/penpot/penpot/pull/10639))
- Fix self-hosted env-generated config.js being cached for 7 days so PENPOT_FLAGS changes did not reach already-cached browsers (by @filipsajdak) [#10556](https://github.com/penpot/penpot/issues/10556) (PR: [#11146](https://github.com/penpot/penpot/pull/11146))
- Fix color of selected text in light theme [#10570](https://github.com/penpot/penpot/issues/10570) (PR: [#10614](https://github.com/penpot/penpot/pull/10614))
- Fix margin input order being inconsistent with padding inputs and between collapsed and expanded states [#10578](https://github.com/penpot/penpot/issues/10578) (PR: [#10797](https://github.com/penpot/penpot/pull/10797))
- Fix uncaught DOMException when writing image/svg+xml content to the clipboard (by @AKnassa) [#10596](https://github.com/penpot/penpot/issues/10596) (PR: [#10663](https://github.com/penpot/penpot/pull/10663))
- Fix tick icons not aligned in the font selector [#10597](https://github.com/penpot/penpot/issues/10597) (PR: [#10774](https://github.com/penpot/penpot/pull/10774))
- Fix incorrect padding values when multiple shapes are selected [#10598](https://github.com/penpot/penpot/issues/10598) (PR: [#10602](https://github.com/penpot/penpot/pull/10602))
- Fix integrity errors related to variants not being repaired [#10606](https://github.com/penpot/penpot/issues/10606) (PR: [#10768](https://github.com/penpot/penpot/pull/10768))
- Fix changing password showing 'Password should be at least 8 characters' error on the old password field (by @AKnassa) [#10626](https://github.com/penpot/penpot/issues/10626) (PR: [#10661](https://github.com/penpot/penpot/pull/10661))
- Fix stroke caps disappearing when dragging [#10633](https://github.com/penpot/penpot/issues/10633) (PR: [#10634](https://github.com/penpot/penpot/pull/10634))
- Fix layout padding being saved as string after invalid input in multi-selection, causing persistence errors (by @niwinz) [#10638](https://github.com/penpot/penpot/issues/10638) (PR: [#10758](https://github.com/penpot/penpot/pull/10758))
- Fix inconsistent theme handling between Penpot and plugins [#10676](https://github.com/penpot/penpot/issues/10676) (PR: [#10677](https://github.com/penpot/penpot/pull/10677))
- Fix image stroke (strokeImage) support missing in the plugin API Stroke interface [#10682](https://github.com/penpot/penpot/issues/10682) (PR: [#10683](https://github.com/penpot/penpot/pull/10683))
- Fix SVG images not working as fill in the WebGL renderer [#10705](https://github.com/penpot/penpot/issues/10705) (PR: [#10707](https://github.com/penpot/penpot/pull/10707))
- Fix background blur not working on text shapes [#10706](https://github.com/penpot/penpot/issues/10706) (PR: [#10712](https://github.com/penpot/penpot/pull/10712))
- Fix background blur not applying on strokes [#10713](https://github.com/penpot/penpot/issues/10713) (PR: [#10716](https://github.com/penpot/penpot/pull/10716))
- Fix text shape with empty content breaking workspace updates [#10725](https://github.com/penpot/penpot/issues/10725) (PR: [#10731](https://github.com/penpot/penpot/pull/10731))
- Fix missing SVG option in the file filters when adding an image fill (by @LuBoys) [#10756](https://github.com/penpot/penpot/issues/10756) (PR: [#10771](https://github.com/penpot/penpot/pull/10771))
- Update onboarding image [#10779](https://github.com/penpot/penpot/issues/10779) (PR: [#10783](https://github.com/penpot/penpot/pull/10783))
- Fix main toolbar overlapping the grid edition bar [#10788](https://github.com/penpot/penpot/issues/10788) (PR: [#10789](https://github.com/penpot/penpot/pull/10789))
- Fix WASM renderer panic when the WebGL context is restored mid-reload [#10810](https://github.com/penpot/penpot/issues/10810) (PR: [#10824](https://github.com/penpot/penpot/pull/10824))
- Fix nginx frontend forwarding the client Host header to backend/exporter, breaking Istio strict mTLS routing (by @yamila-moreno) [#10835](https://github.com/penpot/penpot/issues/10835) (PR: [#11233](https://github.com/penpot/penpot/pull/11233))
- Fix tutorial templates with components causing errors [#10839](https://github.com/penpot/penpot/issues/10839)
- Fix plugin 'Try out' flow crashing when projects have not loaded yet [#10858](https://github.com/penpot/penpot/issues/10858) (PR: [#10859](https://github.com/penpot/penpot/pull/10859))
- Fix collapsed Fill color section on the design panel for new texts [#10860](https://github.com/penpot/penpot/issues/10860) (PR: [#10972](https://github.com/penpot/penpot/pull/10972))
- Fix grid item date tooltip in the project view showing 'Will be deleted' instead of creation date (by @0xTHAC0) [#10873](https://github.com/penpot/penpot/issues/10873) (PR: [#11161](https://github.com/penpot/penpot/pull/11161))
- Merge stop and start measurement shortcut to match current behavior [#10884](https://github.com/penpot/penpot/issues/10884) (PR: [#10906](https://github.com/penpot/penpot/pull/10906))
- Fix shape size badge displayed twice when a user with Viewer permissions selects a shape [#10893](https://github.com/penpot/penpot/issues/10893) (PR: [#10985](https://github.com/penpot/penpot/pull/10985))
- Fix main menu being covered by the toolbar [#10902](https://github.com/penpot/penpot/issues/10902) (PR: [#10926](https://github.com/penpot/penpot/pull/10926))
- Fix font family typography asset persisting across files in newly created text layers [#10925](https://github.com/penpot/penpot/issues/10925) (PR: [#11134](https://github.com/penpot/penpot/pull/11134))
- Fix error raised when editing justified text [#10944](https://github.com/penpot/penpot/issues/10944) (PR: [#10945](https://github.com/penpot/penpot/pull/10945))
- Fix MCP WebSocket proxy failing after penpot-mcp container restarts due to stale nginx DNS resolution (by @780Farva) [#10946](https://github.com/penpot/penpot/issues/10946) (PR: [#10947](https://github.com/penpot/penpot/pull/10947))
- Fix verification email address being unreadable due to low-contrast text on the register success page [#10950](https://github.com/penpot/penpot/issues/10950) (PR: [#10965](https://github.com/penpot/penpot/pull/10965))
- Fix image swatches displaying a wrong format in the color picker list view [#10951](https://github.com/penpot/penpot/issues/10951) (PR: [#10975](https://github.com/penpot/penpot/pull/10975))
- Fix text editor crashing when dropping dragged text after selecting all content [#10954](https://github.com/penpot/penpot/issues/10954) (PR: [#10959](https://github.com/penpot/penpot/pull/10959))
- Fix MCP tokens being usable as API access tokens [#10960](https://github.com/penpot/penpot/issues/10960) (PR: [#10962](https://github.com/penpot/penpot/pull/10962))
- Add size limit and rate limiting to the send-user-feedback endpoint [#10979](https://github.com/penpot/penpot/issues/10979) (PR: [#10990](https://github.com/penpot/penpot/pull/10990))
- Fix main menu not keeping alignment when the left sidebar is expanded [#10981](https://github.com/penpot/penpot/issues/10981) (PR: [#10986](https://github.com/penpot/penpot/pull/10986))
- Fix update-profile-props RPC method accepting undocumented keys [#10991](https://github.com/penpot/penpot/issues/10991) (PR: [#10992](https://github.com/penpot/penpot/pull/10992))
- Fix import-binfile RPC method schema accepting a file-id parameter [#10993](https://github.com/penpot/penpot/issues/10993) (PR: [#10994](https://github.com/penpot/penpot/pull/10994))
- Fix assemble-chunks session lookup ignoring the profile-id scope [#11011](https://github.com/penpot/penpot/issues/11011) (PR: [#11012](https://github.com/penpot/penpot/pull/11012))
- Validate font-id team ownership in create-font-variant [#11013](https://github.com/penpot/penpot/issues/11013) (PR: [#11014](https://github.com/penpot/penpot/pull/11014))
- Validate team ownership on file library link endpoints [#11015](https://github.com/penpot/penpot/issues/11015) (PR: [#11016](https://github.com/penpot/penpot/pull/11016))
- Limit object size allocation in the V1 binfile parser [#11017](https://github.com/penpot/penpot/issues/11017) (PR: [#11018](https://github.com/penpot/penpot/pull/11018))
- Limit recursion depth in the Fressian reader [#11019](https://github.com/penpot/penpot/issues/11019) (PR: [#11020](https://github.com/penpot/penpot/pull/11020))
- Limit concurrent imports in the import-binfile RPC method [#11023](https://github.com/penpot/penpot/issues/11023) (PR: [#11024](https://github.com/penpot/penpot/pull/11024))
- Validate content-type on management upload endpoints [#11025](https://github.com/penpot/penpot/issues/11025) (PR: [#11026](https://github.com/penpot/penpot/pull/11026))
- Fix webhook endpoints allowing unauthorized access via creator-id fallback [#11028](https://github.com/penpot/penpot/issues/11028) (PR: [#11029](https://github.com/penpot/penpot/pull/11029))
- Escape markdown in user-controlled fields of Mattermost error notifications [#11033](https://github.com/penpot/penpot/issues/11033) (PR: [#11034](https://github.com/penpot/penpot/pull/11034))
- Enforce file read permission check on asset endpoints [#11035](https://github.com/penpot/penpot/issues/11035) (PR: [#11036](https://github.com/penpot/penpot/pull/11036))
- Add accumulated storage byte quota for media uploads [#11037](https://github.com/penpot/penpot/issues/11037) (PR: [#11038](https://github.com/penpot/penpot/pull/11038))
- Add bounding box dimension limit to exports [#11041](https://github.com/penpot/penpot/issues/11041) (PR: [#11042](https://github.com/penpot/penpot/pull/11042))
- Sanitize embedded scripts in SVG uploads [#11043](https://github.com/penpot/penpot/issues/11043) (PR: [#11044](https://github.com/penpot/penpot/pull/11044))
- Fix duplicate file ID returning inconsistent error responses [#11045](https://github.com/penpot/penpot/issues/11045) (PR: [#11050](https://github.com/penpot/penpot/pull/11050))
- Enforce permission checks in WebSocket subscription handlers [#11052](https://github.com/penpot/penpot/issues/11052) (PR: [#11054](https://github.com/penpot/penpot/pull/11054))
- Fix 'something went wrong' popup when using incremental numerical input interaction [#11053](https://github.com/penpot/penpot/issues/11053) (PR: [#10794](https://github.com/penpot/penpot/pull/10794))
- Enforce password complexity validation on the backend [#11055](https://github.com/penpot/penpot/issues/11055) (PR: [#11059](https://github.com/penpot/penpot/pull/11059))
- Normalize string inputs before processing [#11060](https://github.com/penpot/penpot/issues/11060) (PR: [#11061](https://github.com/penpot/penpot/pull/11061))
- Add cooldown to avoid sending duplicate invitation emails [#11062](https://github.com/penpot/penpot/issues/11062) (PR: [#11063](https://github.com/penpot/penpot/pull/11063))
- Enable SSRF protection for organization SSO validation [#11064](https://github.com/penpot/penpot/issues/11064) (PR: [#11065](https://github.com/penpot/penpot/pull/11065))
- Fix clone-file-media-object allowing to clone media objects from files without read access [#11087](https://github.com/penpot/penpot/issues/11087) (PR: [#11090](https://github.com/penpot/penpot/pull/11090))
- Fix 404 error page logo not visible in dark mode [#11091](https://github.com/penpot/penpot/issues/11091) (PR: [#11167](https://github.com/penpot/penpot/pull/11167))
- Fix incorrect permission handling when creating an invitation [#11098](https://github.com/penpot/penpot/issues/11098) (PR: [#11099](https://github.com/penpot/penpot/pull/11099))
- Reject zero or negative total-chunks values in upload sessions [#11103](https://github.com/penpot/penpot/issues/11103) (PR: [#11104](https://github.com/penpot/penpot/pull/11104))
- Fix import-binfile accepting unsupported version values without validation [#11105](https://github.com/penpot/penpot/issues/11105) (PR: [#11107](https://github.com/penpot/penpot/pull/11107))
- Fix sessions remaining active on other devices after account deletion [#11114](https://github.com/penpot/penpot/issues/11114) (PR: [#11115](https://github.com/penpot/penpot/pull/11115))
- Use random UUIDs for share link IDs instead of a predictable scheme [#11116](https://github.com/penpot/penpot/issues/11116) (PR: [#11117](https://github.com/penpot/penpot/pull/11117))
- Fix plugin manifest fetch hanging indefinitely without timeout [#11119](https://github.com/penpot/penpot/issues/11119) (PR: [#11120](https://github.com/penpot/penpot/pull/11120))
- Use constant-time comparison for shared key authentication [#11121](https://github.com/penpot/penpot/issues/11121) (PR: [#11122](https://github.com/penpot/penpot/pull/11122))
- Fix ESC key not closing the comment input box after posting a comment in the workspace [#11128](https://github.com/penpot/penpot/issues/11128) (PR: [#11131](https://github.com/penpot/penpot/pull/11131))
- Fix token edit modal crashing when resolving tokens with group nodes [#11143](https://github.com/penpot/penpot/issues/11143) (PR: [#11144](https://github.com/penpot/penpot/pull/11144))
- Fix text editor crashing when pasting into an empty text shape [#11149](https://github.com/penpot/penpot/issues/11149) (PR: [#11150](https://github.com/penpot/penpot/pull/11150))
- Fix comment avatars appearing on top of rulers when scrolling the canvas (by @filipsajdak) [#11163](https://github.com/penpot/penpot/issues/11163) (PR: [#11168](https://github.com/penpot/penpot/pull/11168))
- Fix infinite loop of get-teams and get-team-members calls when granting team access from an email link [#11215](https://github.com/penpot/penpot/issues/11215) (PR: [#11223](https://github.com/penpot/penpot/pull/11223))
- Fix RPC requests bypassing rate limiting with fractional bucket refill intervals [#11253](https://github.com/penpot/penpot/issues/11253) (PR: [#11254](https://github.com/penpot/penpot/pull/11254))
- Fix tempfile bucket serving objects to any authenticated user instead of only the uploader [#11269](https://github.com/penpot/penpot/issues/11269) (PR: [#11270](https://github.com/penpot/penpot/pull/11270))
- Fix increasing a value by clicking and dragging in a numeric input [#11274](https://github.com/penpot/penpot/issues/11274) (PR: [#11334](https://github.com/penpot/penpot/pull/11334))
- Fix notification pill rendering unescaped HTML in the detail section when importing tokens [#11276](https://github.com/penpot/penpot/issues/11276) (PR: [#11275](https://github.com/penpot/penpot/pull/11275))
- Fix share-link holders reading pages outside the authorized scope via the get-page RPC command [#11281](https://github.com/penpot/penpot/issues/11281) (PR: [#11284](https://github.com/penpot/penpot/pull/11284))
- Fix incorrect permission handling when managing share links on a file [#11289](https://github.com/penpot/penpot/issues/11289) (PR: [#11290](https://github.com/penpot/penpot/pull/11290))
- Fix backend session remaining valid after logout when the auth-token cookie is replayed [#11316](https://github.com/penpot/penpot/issues/11316) (PR: [#11317](https://github.com/penpot/penpot/pull/11317))
- Fix get-team-invitation-token requiring only read permissions [#11358](https://github.com/penpot/penpot/issues/11358) (PR: [#11359](https://github.com/penpot/penpot/pull/11359))
### :sparkles: New features & Enhancements
- Group toolbar drawing tools into shape and free-draw flyouts [#9316](https://github.com/penpot/penpot/issues/9316) (PR: [#9480](https://github.com/penpot/penpot/pull/9480), [#10354](https://github.com/penpot/penpot/pull/10354))
- Add outline stroke to Paths [#9961](https://github.com/penpot/penpot/issues/9961) (PR: [#8677](https://github.com/penpot/penpot/pull/8677))
- Make throwValidationErrors default to true for v2 manifest plugins [#10401](https://github.com/penpot/penpot/issues/10401) (PR: [#10433](https://github.com/penpot/penpot/pull/10433))
- Add dedicated Line and Arrow drawing tools (by @davidv399) [#9145](https://github.com/penpot/penpot/issues/9145) (PR: [#9146](https://github.com/penpot/penpot/pull/9146))
- Refactor wasm rulers and UI state [#10116](https://github.com/penpot/penpot/issues/10116) (PR: [#10461](https://github.com/penpot/penpot/pull/10461))
- Improve team invitations modal in the dashboard [#10484](https://github.com/penpot/penpot/issues/10484) (PR: [#10459](https://github.com/penpot/penpot/pull/10459))
- Highlight the first matching font in the font list when searching (by @ai-mountain) [#3204](https://github.com/penpot/penpot/issues/3204) (PR: [#9512](https://github.com/penpot/penpot/pull/9512), [#10450](https://github.com/penpot/penpot/pull/10450))
- Preserve token references when copying and pasting properties instead of resolving them to values (by @AKnassa) [#9582](https://github.com/penpot/penpot/issues/9582) (PR: [#10665](https://github.com/penpot/penpot/pull/10665))
- Add waitForLayoutUpdate method to the plugin API [#10136](https://github.com/penpot/penpot/issues/10136) (PR: [#9898](https://github.com/penpot/penpot/pull/9898))
- Show and manage comments while designing in the workspace [#10239](https://github.com/penpot/penpot/issues/10239) (PR: [#10275](https://github.com/penpot/penpot/pull/10275))
- Simplify MCP server configuration for common MCP clients [#10355](https://github.com/penpot/penpot/issues/10355) (PR: [#10604](https://github.com/penpot/penpot/pull/10604))
- Remove misleading MCP client JSON snippet from the key-generated modal (by @Shlok1729) [#10399](https://github.com/penpot/penpot/issues/10399) (PR: [#10415](https://github.com/penpot/penpot/pull/10415))
- Preview font families in the font selector [#10403](https://github.com/penpot/penpot/issues/10403) (PR: [#10411](https://github.com/penpot/penpot/pull/10411))
- Remember expanded/collapsed state of token sets in the color tokens picker (session scope) [#10551](https://github.com/penpot/penpot/issues/10551) (PR: [#10864](https://github.com/penpot/penpot/pull/10864))
- Show token sets in reverse order by default in the color tokens picker (by @rhinocap) [#10552](https://github.com/penpot/penpot/issues/10552) (PR: [#10658](https://github.com/penpot/penpot/pull/10658))
- Add multi-selection and bulk delete support to pages in the workspace sitemap [#10580](https://github.com/penpot/penpot/issues/10580) (PR: [#10581](https://github.com/penpot/penpot/pull/10581))
- Add a grid/list view toggle for files in the dashboard [#10691](https://github.com/penpot/penpot/issues/10691) (PR: [#10692](https://github.com/penpot/penpot/pull/10692))
- Migrate Docker images to Docker Hardened Images (DHI) [#10720](https://github.com/penpot/penpot/issues/10720) (PR: [#10732](https://github.com/penpot/penpot/pull/10732), [#10733](https://github.com/penpot/penpot/pull/10733), [#10734](https://github.com/penpot/penpot/pull/10734))
- Adopt React Aria [#10802](https://github.com/penpot/penpot/issues/10802) (PR: [#10675](https://github.com/penpot/penpot/pull/10675))
- Add plugin API function for awaiting component updates beyond waitForLayoutUpdate [#10927](https://github.com/penpot/penpot/issues/10927) (PR: [#10964](https://github.com/penpot/penpot/pull/10964))
- Emit open-workspace-file audit event with file statistics on workspace load [#11106](https://github.com/penpot/penpot/issues/11106) (PR: [#11138](https://github.com/penpot/penpot/pull/11138))
## 2.17.2
### :bug: Bugs fixed
@@ -3033,7 +3136,7 @@ is a number of cores)
- Enable penpot SVG metadata only when exporting complete files [Taiga #1914](https://tree.taiga.io/project/penpot/us/1914?milestone=295883)
- Export to PDF all artboards of one page [Taiga #1895](https://tree.taiga.io/project/penpot/us/1895)
- Go to a undo step clicking on a history element of the list [Taiga #1374](https://tree.taiga.io/project/penpot/us/1374)
- Increment font size by 10 with shift+arrows [1047](https://github.com/penpot/penpot/issues/1047)
- Increment font size by 10 with shift+arrows [#1047](https://github.com/penpot/penpot/issues/1047)
- New shortcut to detach components Ctrl+Shift+K [Taiga #1799](https://tree.taiga.io/project/penpot/us/1799)
- Set email inputs to type "email", to aid keyboard entry [Taiga #1921](https://tree.taiga.io/project/penpot/issue/1921)
- Use shift+move to move element orthogonally [#823](https://github.com/penpot/penpot/issues/823)
+26
View File
@@ -0,0 +1,26 @@
# HIGHLIGHTS
## 2.17.0
- Background blur is here
- WebGL rendering gets stronger
- MCP connection status and more
- Design tokens: more visible, more user-friendly
## 2.16.0
- Design tokens in the design panel
- Major community contributions
- WebGL rendering (beta)
## 2.15.0
- AI connected to real design context
- Multi-directional workflow
- Your stack, your model, your decision
+1 -1
View File
@@ -160,6 +160,6 @@ This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
Copyright (c) KALEIDOS SUBSIDIARY SL
Copyright (c) KALEIDOS INC Sucursal en España SL
```
Penpot is a Kaleidos [open source project](https://kaleidos.net/)
+10 -10
View File
@@ -17,7 +17,7 @@
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
io.lettuce/lettuce-core {:mvn/version "7.6.0.RELEASE"}
io.lettuce/lettuce-core {:mvn/version "7.7.0.RELEASE"}
;; Minimal dependencies required by lettuce, we need to include them
;; explicitly because clojure dependency management does not support
;; yet the BOM format.
@@ -25,7 +25,7 @@
io.micrometer/micrometer-observation {:mvn/version "1.14.2"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
com.google.guava/guava {:mvn/version "33.6.0-jre"}
com.google.guava/guava {:mvn/version "33.7.1-jre"}
funcool/yetti
{:git/tag "v11.10"
@@ -40,32 +40,32 @@
nrepl/nrepl {:mvn/version "1.7.0"}
org.postgresql/postgresql {:mvn/version "42.7.13"}
org.xerial/sqlite-jdbc {:mvn/version "3.53.2.1"}
org.xerial/sqlite-jdbc {:mvn/version "3.53.4.0"}
com.zaxxer/HikariCP {:mvn/version "7.1.0"}
io.whitfin/siphash {:mvn/version "2.0.0"}
io.whitfin/siphash {:mvn/version "3.0.0"}
buddy/buddy-hashers {:mvn/version "2.0.167"}
buddy/buddy-sign {:mvn/version "3.6.1-359"}
org.passay/passay {:mvn/version "1.6.6"}
org.passay/passay {:mvn/version "2.0.0"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.4"}
org.jsoup/jsoup {:mvn/version "1.23.1"}
org.jsoup/jsoup {:mvn/version "1.23.2"}
at.yawk.lz4/lz4-java
{:mvn/version "1.11.1"}
{:mvn/version "1.11.2"}
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
dawran6/emoji {:mvn/version "0.2.0"}
markdown-clj/markdown-clj {:mvn/version "1.12.8"}
markdown-clj/markdown-clj {:mvn/version "1.12.9"}
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.50.1"}
software.amazon.awssdk/sts {:mvn/version "2.50.1"}}
software.amazon.awssdk/s3 {:mvn/version "2.54.5"}
software.amazon.awssdk/sts {:mvn/version "2.54.5"}}
:paths ["src" "resources" "target/classes"]
:aliases
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; This is an example on how it can be executed:
;; clojure -Scp $(cat classpath) -M dev/script-fix-sobjects.clj
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns user
(:require
@@ -191,7 +191,7 @@
file named “{{file-name|abbreviate:25}}”.
</p>
<p>
Since this file is in your Penpot team, you can provide access by sending a view-only link.
Since this file is in your Personal Projects, you can provide access by sending a view-only link.
This will allow {{requested-by|abbreviate:25}} to view the content without making any changes.
</p>
<p>To proceed, please click the button below to generate and send the view-only link:</p>
@@ -2,7 +2,7 @@ Hello!
{{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have view-only access to the file named “{{file-name|abbreviate:25}}”.
Since this file is in your Penpot team, you can provide access by sending a view-only link. This will allow {{requested-by|abbreviate:25}} to view the content without making any changes.
Since this file is in your Personal Projects, you can provide access by sending a view-only link. This will allow {{requested-by|abbreviate:25}} to view the content without making any changes.
To proceed, please click the link below to generate and send the view-only link:
@@ -191,7 +191,7 @@
“{{file-name|abbreviate:25}}”.
</p>
<p>
Please note that the file is currently in Your Penpot 's team, so direct access cannot be
Please note that the file is currently in Personal Projects, so direct access cannot be
granted. However, you have two options to provide the requested access:
</p>
<ul>
@@ -5,7 +5,7 @@ Hello!
{{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named “{{file-name|abbreviate:25}}”.
Please note that the file is currently in Your Penpot 's team, so direct access cannot be granted. However, you have two options to provide the requested access:
Please note that the file is currently in Personal Projects, so direct access cannot be granted. However, you have two options to provide the requested access:
- Move the File to Another Team:
+2 -5
View File
@@ -13,10 +13,6 @@ export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
# PENPOT_DATABASE_*, PENPOT_REDIS_URI, PENPOT_OBJECTS_STORAGE_*, AWS_*) is owned by
# docker/devenv/defaults.env and injected via the main service's env block.
if [ -f /home/selfsigned.crt ]; then
export NODE_EXTRA_CA_CERTS=/home/selfsigned.crt;
fi
# Background worker flag is per-instance. Defaults to enabled (ws0); ws1+
# overlays set PENPOT_BACKEND_WORKER=false so scheduled and async tasks only
# run on ws0, keeping notification Pub/Sub bound to a single Valkey. See
@@ -48,7 +44,6 @@ export PENPOT_FLAGS="\
enable-user-feedback \
disable-secure-session-cookies \
enable-smtp \
enable-account-lockout \
enable-prepl-server \
enable-urepl-server \
enable-nrepl-server \
@@ -106,3 +101,5 @@ function setup_minio() {
mc alias set penpot-s3/ "${PENPOT_OBJECTS_STORAGE_S3_ENDPOINT}" minioadmin minioadmin -q
mc mb "penpot-s3/${PENPOT_OBJECTS_STORAGE_S3_BUCKET}" -p -q
}
+1 -1
View File
@@ -4,7 +4,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Copyright (c) KALEIDOS SUBSIDIARY SL
# Copyright (c) KALEIDOS INC Sucursal en España SL
import argparse
import json
+12 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.auth
(:require
@@ -14,10 +14,21 @@
:iterations 3
:parallelism 2})
(def ^:private weak-options
{:alg :pbkdf2+sha256
:iterations 100})
(defn derive-password
[password]
(hashers/derive password default-options))
(defn derive-password-weak
"Derives a password using a fast algorithm (pbkdf2+sha256, 100 iterations).
Intended for demo users only — they are already gated behind the
`demo-users` config flag which is disabled in production."
[password]
(hashers/derive password weak-options))
(defn verify-password
[attempt password]
(try
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.auth.ldap
(:require
-132
View File
@@ -1,132 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.auth.login-lockout
"Brute-force protection: per-account failed login counter backed by Redis.
Uses an atomic Lua script for the increment operation to prevent
concurrent login bypass. Stores count:window_start in the value;
time is passed from Clojure (ct/now) via ARGV, keeping it injectable
for tests. When the counter reaches the configured threshold within
the time window, the account is locked out until the window expires."
(:require
[app.common.generic-pool :as gpool]
[app.common.logging :as l]
[app.common.time :as ct]
[app.config :as cf]
[app.redis :as rds]
[app.redis.script :as-alias rscript]
[clojure.string :as str])
(:import
java.lang.AutoCloseable))
(def ^:private key-prefix "penpot.login-lockout.")
(def ^:private lockout-script
{::rscript/name ::login-lockout
::rscript/path "app/auth/login_lockout.lua"})
(defn- get-pool
[cfg]
(:app.redis/pool cfg))
(defn- with-conn
[cfg f]
(let [pool (get-pool cfg)
conn (gpool/get pool)]
(try
(f @conn)
(finally
(.close ^AutoCloseable conn)))))
(defn- parse-value
"Parse stored \"count:window_start\" string. Returns nil if missing
or expired. Throws NumberFormatException for malformed values
(caught by caller → fail-open)."
[value window-ms now-ms]
(when value
(let [sep (str/index-of value ":")]
(when sep
(let [count (Long/parseLong (subs value 0 sep))
start (Long/parseLong (subs value (inc sep)))]
(when (> (+ start window-ms) now-ms)
{:count count :window-start start}))))))
(defn record-failed-attempt!
"Increment the failed-login counter for a profile-id (atomic via Lua).
Returns nil when the flag is disabled or on Redis error (fail-open).
Otherwise returns a map with :count (int), :ttl (int, seconds),
and :locked? (boolean)."
[cfg profile-id]
(when (contains? cf/flags :account-lockout)
(try
(let [threshold (cf/get :login-lockout-max-attempts)
window (cf/get :login-lockout-window)
window-ms (if (integer? window) window (.toMillis window))
_ (assert (>= threshold 1) "login-lockout-max-attempts must be >= 1")
_ (assert (>= window-ms 60000) "login-lockout-window must be >= 60000ms (1 minute)")
key (str key-prefix profile-id)
now-ms (inst-ms (ct/now))
result (with-conn cfg
(fn [conn]
(rds/eval conn
(assoc lockout-script
::rscript/keys [key]
::rscript/vals [threshold window-ms now-ms]))))
[count locked ttl] result]
{:count count
:locked? (= 1 locked)
:ttl (max 0 ttl)})
(catch Exception cause
(l/warn :hint "redis unavailable, failing open on login lockout"
:profile-id (str profile-id)
:cause cause)
nil))))
(defn clear-attempts!
"Clear the failed-login counter (on successful login or password reset).
No-op when the flag is disabled."
[cfg profile-id]
(when (contains? cf/flags :account-lockout)
(try
(with-conn cfg
(fn [conn]
(rds/del conn (str key-prefix profile-id))))
(catch Exception cause
(l/warn :hint "redis unavailable, failed to clear login lockout"
:profile-id (str profile-id)
:cause cause)))))
(defn locked?
"Check whether the account is currently locked out. Does not increment
the counter. Returns {:locked? false} when the flag is disabled or on
Redis error (fail-open). When locked, includes :ttl (seconds remaining)."
[cfg profile-id]
(if (contains? cf/flags :account-lockout)
(try
(let [threshold (cf/get :login-lockout-max-attempts)
window (cf/get :login-lockout-window)
window-ms (if (integer? window) window (.toMillis window))
_ (assert (>= threshold 1) "login-lockout-max-attempts must be >= 1")
_ (assert (>= window-ms 60000) "login-lockout-window must be >= 60000ms (1 minute)")
key (str key-prefix profile-id)
now-ms (inst-ms (ct/now))
result (with-conn cfg
(fn [conn]
(if-let [current (parse-value (rds/get conn key) window-ms now-ms)]
(let [elapsed (- now-ms (:window-start current))
ttl-ms (- window-ms elapsed)
locked? (>= (:count current) threshold)]
(cond-> {:locked? locked?}
locked?
(assoc :ttl (int (Math/ceil (/ ttl-ms 1000.0))))))
{:locked? false})))]
result)
(catch Exception cause
(l/warn :hint "redis unavailable, failing open on lockout check"
:profile-id (str profile-id)
:cause cause)
{:locked? false}))
{:locked? false}))
-46
View File
@@ -1,46 +0,0 @@
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
--
-- Copyright (c) KALEIDOS SUBSIDIARY SL
local key = KEYS[1]
local threshold = tonumber(ARGV[1])
local window_ms = tonumber(ARGV[2])
local now_ms = tonumber(ARGV[3])
local window_sec = math.ceil(window_ms / 1000)
local val = redis.call('GET', key)
local count
local window_start
if val then
local colon = string.find(val, ':')
if colon then
count = tonumber(string.sub(val, 1, colon - 1))
window_start = tonumber(string.sub(val, colon + 1))
else
count = 0
window_start = now_ms
end
else
count = 0
window_start = now_ms
end
if (now_ms - window_start) > window_ms then
count = 0
window_start = now_ms
end
count = count + 1
redis.call('SET', key, count .. ':' .. window_start, 'EX', window_sec)
local locked = 0
local ttl = 0
if count >= threshold then
locked = 1
ttl = math.ceil((window_ms - (now_ms - window_start)) / 1000)
end
return {count, locked, ttl}
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.auth.oidc
"OIDC client implementation."
+9 -6
View File
@@ -2,14 +2,16 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.auth.passwords
"Password strength validation using Passay library."
(:require
[app.common.exceptions :as ex])
(:import
[org.passay CharacterCharacteristicsRule CharacterRule EnglishCharacterData PasswordData]))
[org.passay PasswordData]
[org.passay.data EnglishCharacterData]
[org.passay.rule CharacterCharacteristicsRule CharacterRule]))
(defonce ^:private passay-code->translation-key
{"INSUFFICIENT_LOWERCASE" "errors.weak-password.insufficient-lowercase"
@@ -18,12 +20,13 @@
"INSUFFICIENT_SPECIAL" "errors.weak-password.insufficient-special"})
(defonce ^:private character-characteristics-rule
(doto (CharacterCharacteristicsRule.)
(.setRules [(CharacterRule. EnglishCharacterData/LowerCase 1)
(CharacterCharacteristicsRule.
4
(into-array org.passay.rule.CharacterRule
[(CharacterRule. EnglishCharacterData/LowerCase 1)
(CharacterRule. EnglishCharacterData/UpperCase 1)
(CharacterRule. EnglishCharacterData/Digit 1)
(CharacterRule. EnglishCharacterData/Special 1)])
(.setNumberOfCharacteristics 4)))
(CharacterRule. EnglishCharacterData/Special 1)])))
(defn validate-password
"Validates password strength.
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.binfile.cleaner
"A collection of helpers for perform cleaning of artifacts; mainly
+25 -47
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.binfile.common
"A binfile related file processing common code, used for different
@@ -27,6 +27,7 @@
[app.features.file-migrations :as fmigr]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.storage :as sto]
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.worker :as-alias wrk]
@@ -653,6 +654,27 @@
(db/exec-one! conn ["SET LOCAL idle_in_transaction_session_timeout = 0"])
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])))
(defn invalidate-thumbnails
[cfg file-id]
(let [storage (sto/resolve cfg)
sql-1
(str "update file_tagged_object_thumbnail "
" set deleted_at = now() "
" where file_id=? returning media_id")
sql-2
(str "update file_thumbnail "
" set deleted_at = now() "
" where file_id=? returning media_id")]
(run! #(sto/touch-object! storage %)
(sequence
(keep :media-id)
(concat
(db/exec! cfg [sql-1 file-id])
(db/exec! cfg [sql-2 file-id]))))))
(defn process-file
[cfg {:keys [id] :as file}]
(let [libs (delay (get-resolved-file-libraries cfg file))]
@@ -853,8 +875,8 @@
(defn get-resolved-file-libraries
"Get all file libraries including itself. Returns an instance of
LoadableWeakValueMap that allows do not have strong references to
the loaded libraries and reduce memory pressure on having
all this libraries at the same time on processing file validation
the loaded libraries and reduce possible memory pressure on having
all this libraries loaded at same time on processing file validation
or file migration.
This still requires at least one library at time to be loaded while
@@ -866,47 +888,3 @@
(cons (:id file)))
load-fn #(get-file cfg % :migrate? false)]
(weak/loadable-weak-value-map library-ids load-fn {id file})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; EXTERNAL LIBRARY RESOLUTION HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn slugify-name
"Slugify a library name for cross-environment matching.
Lowercases, replaces non-alphanumeric runs with '-', strips
leading/trailing '-'."
[name]
(str/slug name))
(def ^:private sql:get-files-names
"SELECT id, name FROM file WHERE id = ANY(?)")
(defn get-files-names
"Return [{:id uuid :name string}] for the given file ids."
[cfg ids]
(db/run! cfg
(fn [{:keys [::db/conn]}]
(let [ids-arr (db/create-array conn "uuid" ids)]
(db/exec! conn [sql:get-files-names ids-arr])))))
(def ^:private sql:get-shared-files-for-team
"SELECT f.id, f.name, f.project_id
FROM file AS f
JOIN project AS p ON (p.id = f.project_id)
WHERE p.team_id = ?
AND f.is_shared = true
AND f.deleted_at IS NULL
AND p.deleted_at IS NULL")
(defn get-shared-files-for-team
"Return [{:id uuid :name string}] for all shared files in a team."
[cfg team-id]
(db/run! cfg
(fn [{:keys [::db/conn]}]
(db/exec! conn [sql:get-shared-files-for-team team-id]))))
(defn find-shared-files-by-slug
"Return all shared files in `team-id` whose slugified name equals `slug`."
[cfg team-id slug]
(->> (get-shared-files-for-team cfg team-id)
(filter #(= slug (slugify-name (:name %))))))
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.binfile.migrations
"A binfile related migrations handling"
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.binfile.v1
"A custom, perfromance and efficiency focused binfile format impl"
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.binfile.v2
"A sqlite3 based binary file exportation with support for exportation
+90 -221
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.binfile.v3
"A ZIP based binary file exportation"
@@ -42,6 +42,7 @@
[datoteka.io :as io])
(:import
java.io.File
java.io.FilterInputStream
java.io.InputStream
java.io.OutputStreamWriter
java.lang.AutoCloseable
@@ -67,16 +68,7 @@
[:relations {:optional true}
[:vector
[:tuple ::sm/uuid ::sm/uuid]]]
;; TODO: rename to :links
[:external-libraries {:optional true}
[:vector
[:map
[:id ::sm/uuid]
[:name :string]
[:slug :string]
[:used-by {:optional true} [:vector ::sm/uuid]]]]]])
[:tuple ::sm/uuid ::sm/uuid]]]])
(def ^:private schema:storage-object
[:map {:title "StorageObject"}
@@ -226,12 +218,14 @@
(.flush writer))
(.closeEntry output))
(defn- get-file
[{:keys [::bfc/export-type] :as cfg} file-id]
[{:keys [::bfc/embed-assets ::bfc/include-libraries] :as cfg} file-id]
(let [detach? (= export-type :detach-libraries)
embed? (= export-type :merge-libraries)]
(when (and include-libraries embed-assets)
(throw (IllegalArgumentException.
"the `include-libraries` and `embed-assets` are mutally excluding options")))
(let [detach? (and (not embed-assets) (not include-libraries))]
(db/tx-run! cfg (fn [cfg]
(cond-> (bfc/get-file cfg file-id
{:realize? true
@@ -241,7 +235,7 @@
(-> (ctf/detach-external-references file-id)
(dissoc :libraries))
embed?
embed-assets
(update :data #(bfc/embed-assets cfg % file-id))
:always
@@ -378,34 +372,12 @@
(write-entry! output path encoded-tokens)))))
(defn- export-files
[{:keys [::bfc/ids ::bfc/export-type ::output] :as cfg}]
(let [original-ids ids
ids (into ids (when (= export-type :include-libraries) (bfc/get-libraries cfg ids)))
rels (if (= export-type :include-libraries)
[{:keys [::bfc/ids ::bfc/include-libraries ::output] :as cfg}]
(let [ids (into ids (when include-libraries (bfc/get-libraries cfg ids)))
rels (if include-libraries
(->> (bfc/get-files-rels cfg ids)
(mapv (juxt :file-id :library-file-id)))
[])
;; Compute external libraries: referenced by original files but
;; not included in the export set. Only relevant for :link-later.
external-libs
(when (= export-type :link-later)
(let [original-rels (bfc/get-files-rels cfg original-ids)
lib-ids (into #{} (map :library-file-id) original-rels)]
(when (seq lib-ids)
(let [lib-names (bfc/get-files-names cfg lib-ids)]
(->> lib-names
(mapv (fn [{:keys [id name]}]
(let [slug (bfc/slugify-name name)]
(when-not (str/blank? slug)
{:id id
:name name
:slug slug
:used-by (->> original-rels
(filter #(= (:library-file-id %) id))
(mapv :file-id))}))))
(filterv some?))))))]
[])]
(vswap! bfc/*state* assoc :files (d/ordered-map))
@@ -418,14 +390,12 @@
;; Write manifest file
(let [files (:files @bfc/*state*)
params (cond-> {:type "penpot/export-files"
:version 1
:generated-by (str "penpot/" (:full cf/version))
:referer "penpot"
:files (vec (vals files))
:relations rels}
(seq external-libs)
(assoc :external-libraries external-libs))]
params {:type "penpot/export-files"
:version 1
:generated-by (str "penpot/" (:full cf/version))
:referer "penpot"
:files (vec (vals files))
:relations rels}]
(write-entry! output "manifest.json" params))))
;; --- IMPORT IMPL
@@ -461,6 +431,31 @@
[^ZipFile input ^ZipEntry entry]
(.getInputStream input entry))
(defn- size-limiting-stream
"Wraps an InputStream to enforce a maximum number of decompressed bytes.
Raises :validation :max-file-size-reached when the limit is exceeded."
^InputStream
[^InputStream input ^long max-size]
(let [counter (atom 0)]
(proxy [FilterInputStream] [input]
(read
([]
(let [b (.read input)]
(when (pos? b)
(when (> (swap! counter inc) max-size)
(ex/raise :type :validation
:code :max-file-size-reached
:hint (str "stream exceeded max size: " max-size))))
b))
([buf off len]
(let [n (.read input buf off len)]
(when (pos? n)
(when (> (swap! counter + (long n)) max-size)
(ex/raise :type :validation
:code :max-file-size-reached
:hint (str "stream exceeded max size: " max-size))))
n))))))
(defn- zip-entry-reader
[^ZipFile input ^ZipEntry entry]
(-> (zip-entry-stream input entry)
@@ -469,10 +464,12 @@
(defn- zip-entry-storage-content
"Wraps a ZipFile and ZipEntry into a penpot storage compatible
object and avoid creating temporal objects"
[input entry]
(let [hash (delay (->> entry
(zip-entry-stream input)
(sto.impl/calculate-hash)))]
[input entry & {:keys [max-size]}]
(let [stream-fn (fn []
(cond-> (zip-entry-stream input entry)
max-size (size-limiting-stream max-size)))
hash (delay (->> (stream-fn)
(sto.impl/calculate-hash)))]
(reify
sto.impl/IContentObject
(get-size [_]
@@ -489,7 +486,7 @@
(throw (UnsupportedOperationException. "not implemented")))
(make-input-stream [_ _]
(zip-entry-stream input entry))
(stream-fn))
(make-output-stream [_ _]
(throw (UnsupportedOperationException. "not implemented"))))))
@@ -866,13 +863,6 @@
[{:keys [::bfc/input ::entries ::bfc/timestamp] :as cfg}]
(events/tap :progress {:section :storage-objects})
;; IMPORTANT: we strongly do not reuse the main connection that can
;; run inside a transaction because the storage upload process can
;; fail in the middle of uploading and leave garbage on the underlying
;; backend, if we participate in the main transaction and it aborts
;; we will lose all registry of the pending to reconcile blobs
;; what the storage subsystem registers in other parallel
;; transaction
(let [storage (sto/resolve cfg)
entries (keep (match-storage-entry-fn) entries)]
@@ -884,9 +874,9 @@
ext (cmedia/mtype->extension (:content-type object))
path (str "objects/" id ext)
content (->> path
(get-zip-entry input)
(zip-entry-storage-content input))]
content (zip-entry-storage-content input
(get-zip-entry input path)
:max-size (::bfc/import-max-object-size cfg))]
(when (not= (:size object) (sto/get-size content))
(ex/raise :type :validation
@@ -896,6 +886,15 @@
:expected-size (:size object)
:found-size (sto/get-size content)))
(when-let [max (::bfc/import-max-object-size cfg)]
(when (> (sto/get-size content) max)
(ex/raise :type :validation
:code :max-file-size-reached
:hint (str "storage object exceeds maximum size: " (sto/get-size content))
:path path
:max max
:found (sto/get-size content))))
(when-let [hash (get object :hash)]
(when (not= hash (sto/get-hash content))
(ex/raise :type :validation
@@ -920,104 +919,6 @@
(vswap! bfc/*state* update :index assoc id (:id sobject)))))))
(defn- add-to-file
"Add a resolved library entry to a file in the file-grouped resolution.
`key` is :done (auto-linked) or :pending (needs resolution)."
[acc file-id file-name key entry]
(update acc file-id (fn [file]
(let [file (or file {:id file-id
:name file-name
:done []
:pending []})]
(update file key conj entry)))))
(defn- compute-link-decisions
"Returns a map of {old-lib-id -> {:library-id ... :library ...}} for external
libraries that should be auto-linked (single candidate AND importer has edit
permission). Libraries with zero or multiple candidates, or where the importer
lacks permission, are excluded — their refs should remain dangling."
[{:keys [::db/conn ::manifest ::bfc/team-id ::bfc/profile-id] :as cfg}]
(reduce
(fn [acc ext-lib]
(let [slug (:slug ext-lib)]
(if (nil? slug)
acc
(let [matching (into [] (bfc/find-shared-files-by-slug cfg team-id slug))]
(if (not= 1 (count matching))
acc
(let [library (first matching)
perms (bfc/get-file-permissions conn profile-id (:id library))]
(if (:can-edit perms)
(assoc acc (:id ext-lib) {:library-id (:id library)
:library library})
acc)))))))
{}
(:external-libraries manifest)))
(defn- resolve-and-link-libraries
"For each external library in the manifest, resolve candidates by slug.
Auto-links single matches (creating DB rows) and builds a file-grouped
resolution map keyed by imported file-id (new UUID)."
[{:keys [::db/conn ::manifest ::bfc/team-id ::bfc/timestamp] :as cfg} files-info]
(assert (uuid? team-id) "team-id should be provided")
(let [file-ids (keys files-info)
decisions (compute-link-decisions cfg)]
(reduce
(fn [acc ext-lib]
(assert (contains? ext-lib :id) "expected `:id` on ext-lib")
(assert (contains? ext-lib :name) "expected `:name` on ext-lib")
(assert (contains? ext-lib :used-by) "expected `:used-by` on ext-lib")
(assert (contains? ext-lib :slug) "expected `:slug` on ext-lib")
(let [used-by (into #{} (map bfc/lookup-index) (:used-by ext-lib))]
(cond
;; No slug → skip
(nil? (:slug ext-lib))
acc
;; Has decision → auto-link (single match + can-edit)
(contains? decisions (:id ext-lib))
(let [{:keys [library-id]} (get decisions (:id ext-lib))
used-by (filter used-by file-ids)]
(doseq [file-id used-by]
(let [rel-params {:file-id file-id :library-file-id library-id}]
(db/insert! conn :file-library-rel rel-params
{::db/on-conflict-do-nothing? true})
(bfc/upsert-file-library-sync! conn (assoc rel-params :synced-at timestamp))))
(let [entry {:id (:id ext-lib)
:name (:name ext-lib)
:linked-to library-id}]
(reduce (fn [acc file-id]
(add-to-file acc file-id (get files-info file-id) :done entry))
acc used-by)))
;; Has candidates but no decision → multi-match or no permission → pending
:else
(let [matching-libraries (into [] (bfc/find-shared-files-by-slug cfg team-id (:slug ext-lib)))]
(if (empty? matching-libraries)
acc
(let [candidates (mapv (fn [lib]
(let [project-id (:project-id lib)
project (bfc/get-project cfg project-id)
project-name (:name project)]
{:id (:id lib)
:name (:name lib)
:project-id project-id
:project-name project-name}))
matching-libraries)
entry {:id (:id ext-lib)
:name (:name ext-lib)
:candidates candidates}]
(reduce (fn [acc file-id]
(add-to-file acc file-id (get files-info file-id) :pending entry))
acc used-by)))))))
{}
(:external-libraries manifest))))
(defn- import-files*
[{:keys [::manifest] :as cfg}]
(bfc/disable-database-timeouts! cfg)
@@ -1026,58 +927,18 @@
(import-storage-objects cfg)
;; Pre-resolve external libraries and add their id mappings to the index
;; BEFORE importing files. This allows relink-refs (inside process-file)
;; to correctly remap :component-file references to the destination library.
;; Only remap when a link will actually be created (single match + can-edit).
(let [decisions (compute-link-decisions cfg)]
(doseq [[old-lib-id {:keys [library-id]}] decisions]
(l/trc :hint "pre-resolving external library"
:old-id (str old-lib-id)
:new-id (str library-id))
(vswap! bfc/*state* update :index assoc old-lib-id library-id)))
(let [files (get manifest :files)
file-ids (reduce (fn [result file]
(let [name' (get file :name)
file (assoc file :name name')]
(conj result (import-file cfg file))))
[]
files)
;; Build map of file-id to file-name for resolution
files-info (into {} (map (fn [file-id manifest-file]
[file-id (:name manifest-file)])
file-ids
files))]
(let [files (get manifest :files)
result (reduce (fn [result file]
(let [name' (get file :name)
file (assoc file :name name')]
(conj result (import-file cfg file))))
[]
files)]
(import-file-relations cfg)
(bfm/apply-pending-migrations! cfg)
(let [resolution (resolve-and-link-libraries cfg files-info)]
(bfm/apply-pending-migrations! cfg)
{:file-ids file-ids
:resolution resolution})))
(defn- invalidate-thumbnails
[cfg file-id]
(let [storage (sto/resolve cfg ::db/reuse-conn true)
sql-1
(str "update file_tagged_object_thumbnail "
" set deleted_at = now() "
" where file_id=? returning media_id")
sql-2
(str "update file_thumbnail "
" set deleted_at = now() "
" where file_id=? returning media_id")]
(run! #(sto/touch-object! storage %)
(sequence
(keep :media-id)
(concat
(db/exec! cfg [sql-1 file-id])
(db/exec! cfg [sql-2 file-id]))))))
result))
(defn- import-file-and-overwrite*
[{:keys [::manifest ::bfc/file-id] :as cfg}]
@@ -1102,11 +963,10 @@
(import-storage-objects cfg)
(import-file cfg file)
(invalidate-thumbnails cfg file-id)
(bfc/invalidate-thumbnails cfg file-id)
(bfm/apply-pending-migrations! cfg)
{:file-ids [file-id]
:resolution {}})))
[file-id])))
(defn- import-files
[{:keys [::bfc/timestamp ::bfc/input] :or {timestamp (ct/now)} :as cfg}]
@@ -1117,6 +977,15 @@
(let [manifest (-> (read-manifest input)
(validate-manifest))
entries (read-zip-entries input)
_ (when-let [max (::bfc/import-max-zip-entries cfg)]
(when (> (count entries) max)
(ex/raise :type :validation
:code :too-many-zip-entries
:hint (str "zip file has too many entries: " (count entries))
:max max
:found (count entries))))
cfg (-> cfg
(assoc ::entries entries)
(assoc ::manifest manifest)
@@ -1154,11 +1023,12 @@
"Do the exportation of a specified file in custom penpot binary
format. There are some options available for customize the output:
`::bfc/export-type`: determines how linked libraries are handled.
Valid values: `:include-libraries` (include linked libraries),
`:merge-libraries` (embed library assets in the file),
`:detach-libraries` (treat assets as basic objects),
`:link-later` (preserve component metadata for relinking on import)."
`::bfc/include-libraries`: additionally to the specified file, all the
linked libraries also will be included (including transitive
dependencies).
`::bfc/embed-assets`: instead of including the libraries, embed in the
same file library all assets used from external libraries."
[{:keys [::bfc/ids] :as cfg} output]
@@ -1174,7 +1044,6 @@
tp (ct/tpoint)
ab (volatile! false)
cs (volatile! nil)]
(try
(l/info :hint "start exportation" :export-id (str id))
(binding [bfc/*state* (volatile! (bfc/initial-state))]
+9 -7
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.config
(:refer-clojure :exclude [get])
@@ -69,9 +69,6 @@
:profile-bounce-max-age (ct/duration {:days 7})
:profile-bounce-threshold 10
:login-lockout-max-attempts 5
:login-lockout-window (ct/duration "15m")
:telemetry-uri "https://telemetry.penpot.app/"
:media-max-file-size (* 1024 1024 30) ; 30MiB
@@ -97,7 +94,11 @@
;; SSRF protection
:ssrf-allowed-hosts #{}
:ssrf-extra-blocked-cidrs #{}})
:ssrf-extra-blocked-cidrs #{}
;; Binfile import limits
:binfile-import-max-object-size (* 1024 1024 100) ;; 100 MiB
:binfile-import-max-zip-entries (* 500 1000)}) ;; 500,000
(def schema:config
(do #_sm/optional-keys
@@ -154,8 +155,9 @@
[:media-processing-service-uri {:optional true} ::sm/uri]
[:media-processing-service-timeout {:optional true} ::sm/int]
[:login-lockout-max-attempts {:optional true} ::sm/int]
[:login-lockout-window {:optional true} ::ct/duration]
;; Binfile import limits (PENPOT_BINFILE_IMPORT_*)
[:binfile-import-max-object-size {:optional true} ::sm/int]
[:binfile-import-max-zip-entries {:optional true} ::sm/int]
[:deletion-delay {:optional true} ::ct/duration]
[:file-clean-delay {:optional true} ::ct/duration]
+6 -6
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.db
(:refer-clojure :exclude [get run!])
@@ -31,8 +31,8 @@
com.zaxxer.hikari.HikariDataSource
com.zaxxer.hikari.HikariPoolMXBean
com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory
io.whitfin.siphash.SipHasher
io.whitfin.siphash.SipHasherContainer
io.whitfin.siphash.SipHash
io.whitfin.siphash.SipHashContext
java.io.InputStream
java.io.OutputStream
java.sql.Connection
@@ -701,12 +701,12 @@
;; --- Locks
(def ^:private siphash-state
(SipHasher/container
(uuid/get-bytes uuid/zero)))
(SipHash/context
(uuid/get-bytes uuid/zero)))
(defn uuid->hash-code
[o]
(.hash ^SipHasherContainer siphash-state
(.hash ^SipHashContext siphash-state
^bytes (uuid/get-bytes o)))
(defn- xact-check-param
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.db.sql
(:refer-clojure :exclude [update])
+3 -3
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.email
"Main api for send emails."
@@ -505,13 +505,13 @@
:schema schema:request-file-access))
(def request-file-access-yourpenpot
"File access on Your Penpot request email."
"File access on Personal Projects request email."
(template-factory
:id ::request-file-access-yourpenpot
:schema schema:request-file-access))
(def request-file-access-yourpenpot-view
"File access on Your Penpot view mode request email."
"File access on Personal Projects view mode request email."
(template-factory
:id ::request-file-access-yourpenpot-view
:schema schema:request-file-access))
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.email.blacklist
"Email blacklist provider"
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.email.whitelist
"Email whitelist provider"
+1 -8
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.features.fdata
"A `fdata/*` related feature migration helpers"
@@ -151,13 +151,6 @@
(cond
(= backend "storage")
;; IMPORTANT: we strongly do not reuse the main connection that can
;; run inside a transaction because the storage upload process can
;; fail in the middle of uploading and leave garbage on the underlying
;; backend, if we participate in the main transaction and it aborts
;; we will lose all registry of the pending to reconcile blobs
;; what the storage subsystem registers in other parallel
;; transaction
(let [storage (sto/resolve cfg)
content (sto/content data)
sobject (sto/put-object! storage
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.features.file-migrations
"Backend specific code for file migrations. Implemented as permanent feature of files."
+2 -5
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.features.file-snapshots
(:require
@@ -326,11 +326,8 @@
(let [file (d/update-when row :metadata fdata/decode-metadata)
vern (rand-int Integer/MAX_VALUE)
;; We reuse the main connection here for storage operations
;; becaue the main operations are touching and we need them
;; to be atomic with the current transaction
storage
(sto/resolve cfg ::db/reuse-conn true)
(sto/resolve cfg {::db/reuse-conn true})
snapshot
(get-snapshot cfg file-id snapshot-id)]
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.features.logical-deletion
"A code related to handle logical deletion mechanism"
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.http
(:require
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.http.access-token
(:require
+3 -11
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.http.assets
"Assets related handlers."
@@ -55,21 +55,13 @@
[{:keys [::sto/storage ::signature-max-age ::cache-max-age] :as cfg} obj]
(let [sig-max-age (or signature-max-age default-signature-max-age)
cch-max-age (or cache-max-age default-cache-max-age)
{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age sig-max-age})
bucket (-> obj meta :bucket)
public? (contains? public-buckets bucket)
;; The disposition is also signed into the presigned url: this
;; response is a redirect, so the header below applies to the
;; redirect itself and not to the bytes the client then fetches
;; from the object store.
{:keys [host port] :as url} (sto/get-object-url storage obj
(cond-> {:max-age sig-max-age}
(not public?)
(assoc :content-disposition "attachment")))
headers (cond-> {"location" (str url)
"x-host" (cond-> host port (str ":" port))
"x-mtype" (-> obj meta :content-type)
"cache-control" (str "max-age=" (inst-ms cch-max-age))}
(not public?)
(not (contains? public-buckets bucket))
(assoc "content-disposition" "attachment"))]
{::yres/status 307
::yres/headers headers}))
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.http.awsns
"AWS SNS webhook handler for bounces."
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.http.client
"Http client abstraction layer.
+7 -3
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.http.debug
(:refer-clojure :exclude [error-handler])
@@ -322,7 +322,9 @@
::bfc/overwrite false
::bfc/profile-id profile-id
::bfc/project-id project-id
::bfc/input path)]
::bfc/input path
::bfc/import-max-object-size (cf/get :binfile-import-max-object-size)
::bfc/import-max-zip-entries (cf/get :binfile-import-max-zip-entries))]
(bf.v3/import-files! cfg)
{::yres/status 200
::yres/headers {"content-type" "text/plain"}
@@ -358,7 +360,9 @@
::bfc/profile-id profile-id
::bfc/project-id project-id
::bfc/input path
::bfc/features (cfeat/get-team-enabled-features cf/flags team))]
::bfc/features (cfeat/get-team-enabled-features cf/flags team)
::bfc/import-max-object-size (cf/get :binfile-import-max-object-size)
::bfc/import-max-zip-entries (cf/get :binfile-import-max-zip-entries))]
(if (= format :binfile-v3)
(bf.v3/import-files! cfg)
+3 -6
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.http.errors
"A errors handling for the http server."
@@ -71,12 +71,9 @@
(defmethod handle-error :rate-limit
[err _ _]
(let [data (ex-data err)]
(let [headers (-> err ex-data ::http/headers)]
{::yres/status 429
::yres/body {:type :rate-limit
:code (:code data)
:hint (:hint data)
:ttl (:ttl data)}}))
::yres/headers headers}))
(defmethod handle-error :concurrency-limit
[err _ _]
+2 -2
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.http.management
"Internal mangement HTTP API"
@@ -209,7 +209,7 @@
[:enum
"customer_service"
"low_quality"
"missing_feature"
"missing_features"
"other"
"switched_service"
"too_complex"
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.http.middleware
(:require
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.http.security
"Additional security layer middlewares"
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.http.session
(:refer-clojure :exclude [read])
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.http.sse
"SSE (server sent events) helpers"
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.http.websocket
"A penpot notification service for file cooperative edition."
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.loggers.audit
"Services related to the user activity (audit log)."
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.loggers.audit.archive-task
(:require
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.loggers.audit.gc-task
(:require
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.loggers.database
"A specific logger impl that persists errors on the database."
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.loggers.mattermost
"A mattermost integration for error reporting."
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.loggers.webhooks
"A mattermost integration for error reporting."
+6 -10
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.main
(:require
@@ -37,7 +37,6 @@
[app.storage.fs :as-alias sto.fs]
[app.storage.gc-deleted :as-alias sto.gc-deleted]
[app.storage.gc-touched :as-alias sto.gc-touched]
[app.storage.pending-gc :as-alias sto.pending-gc]
[app.storage.s3 :as-alias sto.s3]
[app.system :as sys]
[app.util.cron]
@@ -200,10 +199,6 @@
::sto.gc-touched/handler
{::db/pool (ig/ref ::db/pool)}
::sto.pending-gc/handler
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
::http.client/client
{}
@@ -391,13 +386,14 @@
:upload-session-gc (ig/ref :app.tasks.upload-session-gc/handler)
:storage-gc-deleted (ig/ref ::sto.gc-deleted/handler)
:storage-gc-touched (ig/ref ::sto.gc-touched/handler)
:storage-pending-gc (ig/ref ::sto.pending-gc/handler)
:session-gc (ig/ref ::session.tasks/gc)
:audit-log-archive (ig/ref :app.loggers.audit.archive-task/handler)
:audit-log-gc (ig/ref :app.loggers.audit.gc-task/handler)
:delete-object
(ig/ref :app.tasks.delete-object/handler)
:demo-purge
(ig/ref :app.tasks.demo-purge/handler)
:process-webhook-event
(ig/ref ::webhooks/process-event-handler)
:run-webhook
@@ -435,6 +431,9 @@
:app.tasks.delete-object/handler
{::db/pool (ig/ref ::db/pool)}
:app.tasks.demo-purge/handler
{::db/pool (ig/ref ::db/pool)}
:app.tasks.file-gc/handler
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
@@ -551,9 +550,6 @@
{:cron #penpot/cron "0 0 0 * * ?" ;; daily
:task :storage-gc-touched}
{:cron #penpot/cron "0 0 0 * * ?" ;; daily
:task :storage-pending-gc}
{:cron #penpot/cron "0 0 0 * * ?" ;; daily
:task :tasks-gc}
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.media
"Media & Font postprocessing.
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.media.local
"Local media processing via ImageMagick and FontForge shell commands."
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.media.remote
"Remote media processing via the media-processor HTTP service."
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.media.sanitize
"Image EOF truncation helpers strips trailing data after image EOF
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.media.svg
"SVG parsing, sanitization, and info extraction.
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.media.validation
"Schemas and validation functions for media uploads.
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.metrics
(:refer-clojure :exclude [run!])
+2 -5
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.migrations
(:require
@@ -499,10 +499,7 @@
:fn (mg/resource "app/migrations/sql/0152-improve-uuid-defaults-and-drop-extension.sql")}
{:name "0152-rename-version-and-add-indexes-to-server-error-report"
:fn (mg/resource "app/migrations/sql/0152-rename-version-and-add-indexes-to-server-error-report.sql")}
{:name "0153-add-storage-object-status-and-deletion-attempts"
:fn (mg/resource "app/migrations/sql/0153-add-storage-object-status-and-deletion-attempts.sql")}])
:fn (mg/resource "app/migrations/sql/0152-rename-version-and-add-indexes-to-server-error-report.sql")}])
(defn apply-migrations!
[pool name migrations]
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.migrations.clj.migration-0023
(:require
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.migrations.clj.migration-0145
"Migrate plugins references on profiles"
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.migrations.media-refs
"A media refs migration fixer script"
@@ -1,24 +0,0 @@
--- Add a status column and a deletion attempts counter to storage_object.
--- The status column tracks the write-ahead lifecycle of newly created
--- objects. A row is inserted as 'pending' before its blob is written to
--- the underlying storage subsystem and promoted to 'valid' once the
--- write succeeds. Rows in 'pending' state are excluded from the normal
--- lifecycle (deduplication, gc, reads) until they become valid; a
--- periodic task (:storage-pending-gc) reclaims pending rows that were
--- never promoted (e.g. after a crash).
ALTER TABLE storage_object
ADD COLUMN status text NOT NULL DEFAULT 'valid'
CHECK (status IN ('valid', 'pending'));
CREATE INDEX storage_object__status_created_at__idx
ON storage_object (status, created_at)
WHERE status = 'pending';
--- The deletion_attempts counter tracks how many times the gc_deleted
--- task has attempted to physically delete the blob. After max attempts
--- the row is removed and the blob is left as an orphan.
ALTER TABLE storage_object
ADD COLUMN deletion_attempts bigint NOT NULL DEFAULT 0;
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.msgbus
"The msgbus abstraction implemented using redis as underlying backend."
+2 -2
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC
(ns app.nitrate
"Module that make calls to the external nitrate aplication"
@@ -245,7 +245,7 @@
[:enum
"customer_service"
"low_quality"
"missing_feature"
"missing_features"
"other"
"switched_service"
"too_complex"
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.redis
"The msgbus abstraction implemented using redis as underlying backend."
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.rpc
(:require
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.rpc.climit
"Concurrencly limiter for RPC."
Loaded 100 of 1834 files, more files were not shown because too many files have changed in this diff. Show more