mirror of
https://github.com/penpot/penpot.git
synced 2026-09-02 18:18:41 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
f5aad7b1ae
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
111
CHANGES.md
111
CHANGES.md
@ -31,18 +31,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
|
||||
@ -3053,7 +3156,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)
|
||||
|
||||
@ -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"}
|
||||
|
||||
com.ladybugdb/lbug {:mvn/version "0.19.1"}
|
||||
;; Required by Arrow RootAllocator (lbug only pulls arrow-memory-core).
|
||||
|
||||
@ -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
|
||||
|
||||
@ -9,7 +9,9 @@
|
||||
(: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.
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
[datoteka.io :as io])
|
||||
(:import
|
||||
java.io.File
|
||||
java.io.FilterInputStream
|
||||
java.io.InputStream
|
||||
java.io.OutputStreamWriter
|
||||
java.lang.AutoCloseable
|
||||
@ -461,6 +462,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 +495,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 +517,7 @@
|
||||
(throw (UnsupportedOperationException. "not implemented")))
|
||||
|
||||
(make-input-stream [_ _]
|
||||
(zip-entry-stream input entry))
|
||||
(stream-fn))
|
||||
(make-output-stream [_ _]
|
||||
(throw (UnsupportedOperationException. "not implemented"))))))
|
||||
|
||||
@ -884,9 +912,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 +924,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
|
||||
@ -1117,6 +1154,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)
|
||||
|
||||
@ -94,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
|
||||
@ -151,6 +155,10 @@
|
||||
[:media-processing-service-uri {:optional true} ::sm/uri]
|
||||
[:media-processing-service-timeout {:optional true} ::sm/int]
|
||||
|
||||
;; 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]
|
||||
[:telemetry-enabled {:optional true} ::sm/boolean]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -370,7 +370,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"}
|
||||
@ -626,7 +628,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)
|
||||
|
||||
@ -400,6 +400,8 @@
|
||||
|
||||
: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
|
||||
@ -437,6 +439,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)}
|
||||
|
||||
@ -103,7 +103,9 @@
|
||||
(assoc ::bfc/project-id project-id)
|
||||
(assoc ::bfc/profile-id profile-id)
|
||||
(assoc ::bfc/team-id (:id team))
|
||||
(assoc ::bfc/name name))
|
||||
(assoc ::bfc/name name)
|
||||
(assoc ::bfc/import-max-object-size (cf/get :binfile-import-max-object-size))
|
||||
(assoc ::bfc/import-max-zip-entries (cf/get :binfile-import-max-zip-entries)))
|
||||
|
||||
input-path (:path file)
|
||||
owned? (some? upload-id)
|
||||
|
||||
@ -231,8 +231,11 @@
|
||||
::sm/params schema:get-comment-threads}
|
||||
[cfg {:keys [::rpc/profile-id file-id share-id] :as params}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(get-comment-threads conn profile-id file-id))))
|
||||
(let [perms (files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
threads (get-comment-threads conn profile-id file-id)]
|
||||
(if (= :share-link (:type perms))
|
||||
(filterv #(contains? (:pages perms) (:page-id %)) threads)
|
||||
threads)))))
|
||||
|
||||
(defn- get-comment-threads-sql
|
||||
[where]
|
||||
@ -329,9 +332,15 @@
|
||||
::sm/params schema:get-comment-thread}
|
||||
[cfg {:keys [::rpc/profile-id file-id id share-id] :as params}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(some-> (db/exec-one! conn [sql:get-comment-thread profile-id file-id id])
|
||||
(decode-row)))))
|
||||
(let [perms (files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
thread (some-> (db/exec-one! conn [sql:get-comment-thread profile-id file-id id])
|
||||
(decode-row))]
|
||||
(when (and thread (= :share-link (:type perms)))
|
||||
(when-not (contains? (:pages perms) (:page-id thread))
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "not found")))
|
||||
thread))))
|
||||
|
||||
;; --- COMMAND: Retrieve Comments
|
||||
|
||||
@ -348,8 +357,13 @@
|
||||
::sm/params schema:get-comments}
|
||||
[cfg {:keys [::rpc/profile-id thread-id share-id]}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [{:keys [file-id]} (get-comment-thread conn thread-id)]
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(let [{:keys [file-id page-id]} (get-comment-thread conn thread-id)
|
||||
perms (files/check-comment-permissions! cfg profile-id file-id share-id)]
|
||||
(when (and (= :share-link (:type perms))
|
||||
(not (contains? (:pages perms) page-id)))
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "not found"))
|
||||
(get-comments conn thread-id)))))
|
||||
|
||||
(def sql:get-comments
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
(ns app.rpc.commands.demo
|
||||
"A demo specific mutations."
|
||||
(:require
|
||||
[app.auth :refer [derive-password]]
|
||||
[app.auth :refer [derive-password-weak]]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as audit]
|
||||
@ -17,6 +17,7 @@
|
||||
[app.rpc.commands.auth :as auth]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]
|
||||
[app.worker :as wrk]
|
||||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.nonce :as bn]))
|
||||
|
||||
@ -34,8 +35,8 @@
|
||||
:code :demo-users-not-allowed
|
||||
:hint "Demo users are disabled by config."))
|
||||
|
||||
(let [sem (System/currentTimeMillis)
|
||||
email (str "demo-" sem ".demo@example.com")
|
||||
(let [sem (uuid/next)
|
||||
email (str "demo-" sem "@demo.example.com")
|
||||
fullname (str "Demo User " sem)
|
||||
|
||||
password (-> (bn/random-bytes 16)
|
||||
@ -46,13 +47,17 @@
|
||||
:fullname fullname
|
||||
:is-active true
|
||||
:is-demo true
|
||||
:deleted-at (ct/in-future (cf/get-deletion-delay))
|
||||
:password (derive-password password)
|
||||
:password (derive-password-weak password)
|
||||
:props {}}
|
||||
profile (db/tx-run! cfg (fn [cfg]
|
||||
(->> (auth/create-profile cfg params)
|
||||
(auth/create-profile-rels cfg))))]
|
||||
|
||||
(wrk/submit! (-> cfg
|
||||
(assoc ::wrk/task :demo-purge)
|
||||
(assoc ::wrk/delay (cf/get-deletion-delay))
|
||||
(assoc ::wrk/params {:profile-id (:id profile)})))
|
||||
|
||||
(with-meta {:email email
|
||||
:password password}
|
||||
{::audit/profile-id (:id profile)})))
|
||||
|
||||
|
||||
@ -95,18 +95,23 @@
|
||||
(def check-read-permissions!
|
||||
(perms/make-check-fn has-read-permissions?))
|
||||
|
||||
;; A user has comment permissions if she has read permissions, or
|
||||
;; explicit comment permissions through the share-id
|
||||
;; A user has comment permissions if:
|
||||
;; - For :membership type: they have read permissions OR explicit comment permissions
|
||||
;; - For :share-link type: they must have explicit comment permissions (who-comment=all)
|
||||
;; This prevents share-link holders with who-comment=team from bypassing the restriction
|
||||
|
||||
(defn check-comment-permissions!
|
||||
[cfg profile-id file-id share-id]
|
||||
(let [perms (perms/get-file-read-permissions cfg profile-id file-id share-id)
|
||||
can-read (has-read-permissions? perms)
|
||||
can-comment (has-comment-permissions? perms)]
|
||||
(when-not (or can-read can-comment)
|
||||
(let [perms (perms/get-file-read-permissions cfg profile-id file-id share-id)
|
||||
allowed? (if (= :share-link (:type perms))
|
||||
(has-comment-permissions? perms)
|
||||
(or (has-read-permissions? perms)
|
||||
(has-comment-permissions? perms)))]
|
||||
(when-not allowed?
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "not found"))))
|
||||
:hint "not found"))
|
||||
perms))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUERY COMMANDS
|
||||
|
||||
@ -426,7 +426,9 @@
|
||||
(assoc ::bfc/project-id project-id)
|
||||
(assoc ::bfc/profile-id profile-id)
|
||||
(assoc ::bfc/input template)
|
||||
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team)))
|
||||
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team))
|
||||
(assoc ::bfc/import-max-object-size (cf/get :binfile-import-max-object-size))
|
||||
(assoc ::bfc/import-max-zip-entries (cf/get :binfile-import-max-zip-entries)))
|
||||
|
||||
result (if (= format :binfile-v3)
|
||||
(bf.v3/import-files! cfg)
|
||||
|
||||
@ -141,9 +141,7 @@
|
||||
(defn get-profile
|
||||
"Get profile by id. Throws not-found exception if no profile found."
|
||||
[conn id & {:as opts}]
|
||||
;; NOTE: We need to set ::db/remove-deleted to false because demo profiles
|
||||
;; are created with a set deleted-at value
|
||||
(-> (db/get-by-id conn :profile id (assoc opts ::db/remove-deleted false))
|
||||
(-> (db/get-by-id conn :profile id opts)
|
||||
(decode-row)))
|
||||
|
||||
;; --- MUTATION: Update Profile (own)
|
||||
|
||||
@ -944,8 +944,10 @@
|
||||
::sm/params schema:delete-team-member
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}]
|
||||
(let [team (get-team conn :profile-id profile-id :team-id team-id)
|
||||
perms (get-permissions conn profile-id team-id)]
|
||||
(let [team (get-team conn :profile-id profile-id :team-id team-id)
|
||||
perms (get-permissions conn profile-id team-id)
|
||||
members (get-team-members conn team-id)
|
||||
member (d/seek #(= member-id (:id %)) members)]
|
||||
(when-not (or (:is-owner perms)
|
||||
(:is-admin perms))
|
||||
(ex/raise :type :validation
|
||||
@ -955,6 +957,15 @@
|
||||
(ex/raise :type :validation
|
||||
:code :cant-remove-yourself))
|
||||
|
||||
(when-not member
|
||||
(ex/raise :type :not-found
|
||||
:code :member-does-not-exist))
|
||||
|
||||
(when (and (:is-owner member)
|
||||
(not (:is-owner perms)))
|
||||
(ex/raise :type :validation
|
||||
:code :cant-remove-owner))
|
||||
|
||||
(db/delete! conn :team-profile-rel {:profile-id member-id
|
||||
:team-id team-id})
|
||||
|
||||
|
||||
@ -56,7 +56,7 @@
|
||||
(assoc :can-read true)))
|
||||
|
||||
(defn- get-view-only-bundle
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id file-id ::perms] :as params}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id file-id share-id ::perms] :as params}]
|
||||
(let [file (bfc/get-file cfg file-id)
|
||||
|
||||
project (db/get conn :project
|
||||
@ -89,16 +89,18 @@
|
||||
(mapv (fn [{:keys [id] :as lib}]
|
||||
(merge lib (bfc/get-file cfg id)))))
|
||||
|
||||
links (->> (db/query conn :share-link {:file-id file-id})
|
||||
(mapv (fn [row]
|
||||
(-> row
|
||||
(update :pages db/decode-pgarray #{})
|
||||
;; NOTE: the flags are deprecated but are still present
|
||||
;; on the table on old rows. The flags are pgarray and
|
||||
;; for avoid decoding it (because they are no longer used
|
||||
;; on frontend) we just dissoc the column attribute from
|
||||
;; row.
|
||||
(dissoc :flags)))))
|
||||
links (cond->> (->> (db/query conn :share-link {:file-id file-id})
|
||||
(mapv (fn [row]
|
||||
(-> row
|
||||
(update :pages db/decode-pgarray #{})
|
||||
;; NOTE: the flags are deprecated but are still present
|
||||
;; on the table on old rows. The flags are pgarray and
|
||||
;; for avoid decoding it (because they are no longer used
|
||||
;; on frontend) we just dissoc the column attribute from
|
||||
;; row.
|
||||
(dissoc :flags)))))
|
||||
(= :share-link (:type perms))
|
||||
(filterv #(= (:id %) share-id)))
|
||||
|
||||
fonts (db/query conn :team-font-variant
|
||||
{:team-id (:id team)
|
||||
|
||||
@ -1045,3 +1045,18 @@ RETURNING id, deleted_at;")
|
||||
(update acc :created conj email)))))
|
||||
{:created [] :skipped []}
|
||||
emails)))))
|
||||
|
||||
;; ---- API: get-air-gapped
|
||||
|
||||
(def ^:private schema:get-air-gapped-result
|
||||
[:map
|
||||
[:air-gapped ::sm/boolean]])
|
||||
|
||||
(sv/defmethod ::get-air-gapped
|
||||
"Returns whether this Penpot instance runs in air-gapped mode."
|
||||
{::doc/added "2.18"
|
||||
::sm/params [:map]
|
||||
::sm/result schema:get-air-gapped-result
|
||||
::rpc/auth false}
|
||||
[_cfg _params]
|
||||
{:air-gapped (contains? cf/flags :air-gapped-conf)})
|
||||
|
||||
41
backend/src/app/tasks/demo_purge.clj
Normal file
41
backend/src/app/tasks/demo_purge.clj
Normal file
@ -0,0 +1,41 @@
|
||||
;; 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 INC Sucursal en España SL
|
||||
|
||||
(ns app.tasks.demo-purge
|
||||
"Task handler for delayed demo profile deletion. Submitted at demo
|
||||
creation time with a delay matching the configured deletion-delay."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.worker :as wrk]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(defmethod ig/assert-key ::handler
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expected a valid database pool"))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props]}]
|
||||
(let [profile-id (get props :profile-id)
|
||||
now (ct/now)]
|
||||
|
||||
(l/trc :hint "demo-purge" :profile-id (str profile-id))
|
||||
|
||||
;; Mark the profile for immediate deletion
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(db/update! conn :profile
|
||||
{:deleted-at now}
|
||||
{:id profile-id}
|
||||
{::db/return-keys false})
|
||||
(wrk/submit!
|
||||
(-> cfg
|
||||
(assoc ::wrk/task :delete-object)
|
||||
(assoc ::wrk/params {:object :profile
|
||||
:deleted-at now
|
||||
:id profile-id}))))))))
|
||||
@ -13,6 +13,7 @@
|
||||
[app.db :as db]
|
||||
[app.features.fdata :as fdata]
|
||||
[app.storage :as sto]
|
||||
[app.tasks.delete-object :as dobj]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def ^:private sql:get-profiles
|
||||
@ -33,6 +34,11 @@
|
||||
;; Mark as deleted the storage object
|
||||
(some->> photo-id (sto/touch-object! storage))
|
||||
|
||||
;; Cascade soft-delete to owned teams, projects, files, etc.
|
||||
(dobj/delete-object cfg {:object :profile
|
||||
:id id
|
||||
:deleted-at timestamp})
|
||||
|
||||
(let [affected (-> (db/delete! conn :profile {:id id})
|
||||
(db/get-update-count))]
|
||||
(+ total affected)))
|
||||
|
||||
@ -5,7 +5,12 @@
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.util.ssrf
|
||||
"URL/host validation to prevent Server-Side Request Forgery."
|
||||
"URL/host validation to prevent Server-Side Request Forgery.
|
||||
|
||||
The blocklist covers the standard JVM InetAddress classifications plus
|
||||
explicit ranges: IPv6 ULA, IPv4-mapped loopback, cloud metadata,
|
||||
operator-supplied CIDRs and the IPv6 transition mechanisms NAT64, 6to4
|
||||
and Teredo."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
@ -122,6 +127,20 @@
|
||||
;; Check the embedded IPv4 is loopback (127.x.x.x)
|
||||
(= (bit-and (aget bs 12) 0xFF) 127))))
|
||||
|
||||
(defn- transition-prefix
|
||||
"Classify a 16-byte IPv6 address into its transition mechanism:
|
||||
:nat64 (64:ff9b::/96), :6to4 (2002::/16), :teredo (2001:0000::/32) or nil."
|
||||
[^bytes bs]
|
||||
(let [b0 (bit-and (aget bs 0) 0xFF)
|
||||
b1 (bit-and (aget bs 1) 0xFF)
|
||||
b2 (bit-and (aget bs 2) 0xFF)
|
||||
b3 (bit-and (aget bs 3) 0xFF)]
|
||||
(cond
|
||||
(and (= b0 0x00) (= b1 0x64) (= b2 0xFF) (= b3 0x9B)) :nat64
|
||||
(and (= b0 0x20) (= b1 0x02)) :6to4
|
||||
(and (= b0 0x20) (= b1 0x01) (= b2 0x00) (= b3 0x00)) :teredo
|
||||
:else nil)))
|
||||
|
||||
(defn- blocked-address?
|
||||
"Check if an InetAddress should be blocked. Returns true if blocked."
|
||||
[^InetAddress addr]
|
||||
@ -141,12 +160,15 @@
|
||||
;; Cloud metadata IPs (exact match)
|
||||
(contains? cloud-metadata-ips (.getHostAddress addr))
|
||||
|
||||
;; Extra blocked CIDRs (IPv4 only)
|
||||
;; Extra blocked CIDRs (IPv4 only) and IPv6 transition mechanisms
|
||||
(let [bs (.getAddress addr)]
|
||||
(if (= (alength bs) 4)
|
||||
(or (some #(in-cidr4? bs %) extra-blocked-ranges)
|
||||
(some #(in-cidr4? bs %) extra-blocked-cidrs))
|
||||
false))))
|
||||
;; IPv6 transition mechanisms (NAT64/6to4/Teredo): the range is
|
||||
;; rejected outright.
|
||||
(boolean (when (= (alength bs) 16)
|
||||
(transition-prefix bs)))))))
|
||||
|
||||
(defn resolve-host
|
||||
"Resolve a hostname to all InetAddress objects. Wraps InetAddress/getAllByName
|
||||
@ -163,8 +185,10 @@
|
||||
- host must resolve to at least one address, and
|
||||
- **every** resolved address must NOT be in the blocklist
|
||||
(loopback, link-local, site-local, multicast, any-local,
|
||||
cloud-metadata 169.254.169.254, IPv6 ULA fc00::/7, IPv4-mapped
|
||||
IPv6 of any blocked IPv4, plus operator-supplied CIDRs).
|
||||
cloud-metadata 169.254.169.254, IPv6 ULA fc00::/7, IPv6 transition
|
||||
mechanisms NAT64 64:ff9b::/96, 6to4 2002::/16 and Teredo
|
||||
2001:0000::/32, IPv4-mapped IPv6 of any blocked IPv4,
|
||||
plus operator-supplied CIDRs).
|
||||
When the host is an IP literal (decimal/octal/hex/IPv6) it is
|
||||
normalized via `com.google.common.net.InetAddresses` before the
|
||||
check.
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
[app.storage :as sto]
|
||||
[app.storage.tmp :as tmp]
|
||||
[backend-tests.helpers :as th]
|
||||
[backend-tests.storage-test :as stt]
|
||||
[clojure.test :as t]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.fs :as fs]
|
||||
@ -1883,3 +1884,86 @@
|
||||
(t/is (some? ext-libs)
|
||||
"type :link-later should produce external-libraries even with include-libraries=true")
|
||||
(t/is (pos? (count ext-libs))))))
|
||||
|
||||
(t/deftest import-rejects-too-many-zip-entries
|
||||
;; import must reject ZIP files exceeding max-zip-entries
|
||||
(let [profile (th/create-profile* 1)
|
||||
file (prepare-simple-file profile)
|
||||
output (tmp/tempfile :suffix ".zip")]
|
||||
|
||||
(v3/export-files!
|
||||
(-> th/*system*
|
||||
(assoc ::bfc/ids #{(:id file)})
|
||||
(assoc ::bfc/export-type :detach-libraries))
|
||||
(io/output-stream output))
|
||||
|
||||
;; Import with max-zip-entries=1 — the exported ZIP has more entries
|
||||
(let [cfg (-> th/*system*
|
||||
(assoc ::bfc/project-id (:default-project-id profile))
|
||||
(assoc ::bfc/profile-id (:id profile))
|
||||
(assoc ::bfc/input output)
|
||||
(assoc ::bfc/import-max-zip-entries 1))
|
||||
out (try
|
||||
(v3/import-files! cfg)
|
||||
:no-error
|
||||
(catch Throwable e
|
||||
(let [d (or (ex-data e) (some-> (ex-cause e) ex-data))]
|
||||
d)))]
|
||||
(t/is (= :validation (:type out)))
|
||||
(t/is (= :too-many-zip-entries (:code out))))))
|
||||
|
||||
(defn- prepare-file-with-media
|
||||
"Creates a file with a media object backed by a real storage object,
|
||||
so that v3 export produces objects/ entries."
|
||||
[profile]
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(stt/configure-storage-backend))
|
||||
|
||||
sobject (sto/put-object! storage {::sto/content (sto/content "media-bytes")
|
||||
:content-type "image/svg+xml"
|
||||
:bucket "file-media-object"})
|
||||
|
||||
file (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared false})
|
||||
|
||||
mobj (th/create-file-media-object* {:file-id (:id file)
|
||||
:is-local true
|
||||
:media-id (:id sobject)})]
|
||||
(update-file!
|
||||
:file-id (:id file)
|
||||
:profile-id (:id profile)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes
|
||||
[{:type :add-media
|
||||
:object mobj}])
|
||||
|
||||
(dissoc file :data)))
|
||||
|
||||
(t/deftest import-rejects-oversized-object
|
||||
;; import must reject storage objects exceeding max-object-size
|
||||
(let [profile (th/create-profile* 1)
|
||||
file (prepare-file-with-media profile)
|
||||
output (tmp/tempfile :suffix ".zip")]
|
||||
|
||||
(v3/export-files!
|
||||
(-> th/*system*
|
||||
(assoc ::bfc/ids #{(:id file)})
|
||||
(assoc ::bfc/export-type :detach-libraries))
|
||||
(io/output-stream output))
|
||||
|
||||
;; Import with max-object-size=1 — the media object will exceed this
|
||||
(let [cfg (-> th/*system*
|
||||
(assoc ::bfc/project-id (:default-project-id profile))
|
||||
(assoc ::bfc/profile-id (:id profile))
|
||||
(assoc ::bfc/input output)
|
||||
(assoc ::bfc/import-max-object-size 1))
|
||||
out (try
|
||||
(v3/import-files! cfg)
|
||||
:no-error
|
||||
(catch Throwable e
|
||||
(let [d (or (ex-data e) (some-> (ex-cause e) ex-data))]
|
||||
d)))]
|
||||
(t/is (= :validation (:type out)))
|
||||
(t/is (= :max-file-size-reached (:code out))))))
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
|
||||
(ns backend-tests.db-test
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t])
|
||||
@ -41,3 +42,13 @@
|
||||
|
||||
(t/testing "maximum pool size is reasonable"
|
||||
(t/is (pos? (:maximum-pool-size stats))))))
|
||||
|
||||
(t/deftest uuid->hash-code-is-deterministic
|
||||
(t/is (= (db/uuid->hash-code uuid/zero)
|
||||
(db/uuid->hash-code uuid/zero))))
|
||||
|
||||
(t/deftest uuid->hash-code-returns-long
|
||||
(t/is (instance? Long (db/uuid->hash-code uuid/zero))))
|
||||
|
||||
(t/deftest uuid->hash-code-stable-for-zero-uuid
|
||||
(t/is (= 3659997967308761462 (db/uuid->hash-code uuid/zero))))
|
||||
|
||||
47
backend/test/backend_tests/demo_test.clj
Normal file
47
backend/test/backend_tests/demo_test.clj
Normal file
@ -0,0 +1,47 @@
|
||||
;; 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 INC Sucursal en España SL
|
||||
|
||||
(ns backend-tests.demo-test
|
||||
(:require
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.tasks.demo-purge :as demo-purge]
|
||||
[app.worker :as wrk]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(t/deftest demo-profile-created-without-deleted-at
|
||||
(let [profile (th/create-profile* 999 {:is-demo true})]
|
||||
(t/is (true? (:is-demo profile)))
|
||||
(t/is (nil? (:deleted-at profile)))
|
||||
(t/is (some? (:id profile)))))
|
||||
|
||||
(t/deftest get-profile-finds-demo-user-without-override
|
||||
(let [profile (th/create-profile* 998 {:is-demo true})
|
||||
found (db/run! th/*pool*
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(profile/get-profile conn (:id profile))))]
|
||||
(t/is (some? found))
|
||||
(t/is (= (:id profile) (:id found)))))
|
||||
|
||||
(t/deftest demo-purge-handler-submits-delete-object
|
||||
(let [profile (th/create-profile* 996 {:is-demo true})
|
||||
handler (ig/init-key :app.tasks.demo-purge/handler
|
||||
{::db/pool th/*pool*})
|
||||
submitted (atom nil)]
|
||||
(with-redefs [wrk/submit! (fn [& {:keys [::wrk/task ::wrk/params]}]
|
||||
(reset! submitted {:task task :params params}))]
|
||||
(handler {:props {:profile-id (:id profile)
|
||||
:deleted-at (ct/now)}}))
|
||||
(t/is (= :delete-object (:task @submitted)))
|
||||
(t/is (= :profile (:object (:params @submitted))))
|
||||
(t/is (= (:id profile) (:id (:params @submitted))))
|
||||
(t/is (some? (:deleted-at (:params @submitted))))))
|
||||
51
backend/test/backend_tests/passwords_test.clj
Normal file
51
backend/test/backend_tests/passwords_test.clj
Normal file
@ -0,0 +1,51 @@
|
||||
;; 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 INC Sucursal en España SL
|
||||
|
||||
(ns backend-tests.passwords-test
|
||||
(:require
|
||||
[app.auth.passwords :as passwords]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(defn- run-validation
|
||||
[password]
|
||||
(try
|
||||
(passwords/validate-password password)
|
||||
nil
|
||||
(catch Throwable e
|
||||
e)))
|
||||
|
||||
(t/deftest validate-password-accepts-strong-password
|
||||
(t/is (nil? (run-validation "Str0ng!Pass"))))
|
||||
|
||||
(t/deftest validate-password-rejects-too-short-password
|
||||
(let [error (run-validation "Ab1!x")]
|
||||
(t/is (th/ex-of-code? error :weak-password))
|
||||
(t/is (= ["errors.weak-password.too-short"] (:details (ex-data error))))))
|
||||
|
||||
(t/deftest validate-password-rejects-missing-lowercase
|
||||
(let [error (run-validation "ABCDEFG1!")]
|
||||
(t/is (th/ex-of-code? error :weak-password))
|
||||
(t/is (= ["errors.weak-password.insufficient-lowercase"]
|
||||
(:details (ex-data error))))))
|
||||
|
||||
(t/deftest validate-password-rejects-missing-uppercase
|
||||
(let [error (run-validation "abcdefg1!")]
|
||||
(t/is (th/ex-of-code? error :weak-password))
|
||||
(t/is (= ["errors.weak-password.insufficient-uppercase"]
|
||||
(:details (ex-data error))))))
|
||||
|
||||
(t/deftest validate-password-rejects-missing-digit
|
||||
(let [error (run-validation "Abcdefgh!")]
|
||||
(t/is (th/ex-of-code? error :weak-password))
|
||||
(t/is (= ["errors.weak-password.insufficient-digits"]
|
||||
(:details (ex-data error))))))
|
||||
|
||||
(t/deftest validate-password-rejects-missing-special
|
||||
(let [error (run-validation "Abcdefgh1")]
|
||||
(t/is (th/ex-of-code? error :weak-password))
|
||||
(t/is (= ["errors.weak-password.insufficient-special"]
|
||||
(:details (ex-data error))))))
|
||||
@ -285,3 +285,196 @@
|
||||
|
||||
(let [threads (th/db-query :comment-thread {:file-id (:id file-1)})]
|
||||
(t/is (= 0 (count threads)))))))))
|
||||
|
||||
(t/deftest share-link-who-comment-team-cannot-comment
|
||||
(let [owner (th/create-profile* 1 {:is-active true})
|
||||
outsider (th/create-profile* 2 {:is-active true})
|
||||
|
||||
team (th/create-team* 1 {:profile-id (:id owner)})
|
||||
project (th/create-project* 1 {:team-id (:id team)
|
||||
:profile-id (:id owner)})
|
||||
file (th/create-file* 1 {:profile-id (:id owner)
|
||||
:project-id (:id project)})
|
||||
page-id (get-in file [:data :pages 0])
|
||||
|
||||
share (th/command! {::th/type :create-share-link
|
||||
::rpc/profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:pages #{page-id}
|
||||
:who-comment "team"
|
||||
:who-inspect "all"})
|
||||
share-id (get-in share [:result :id])]
|
||||
|
||||
(t/testing "outsider with who-comment=team share-link cannot get-comment-threads"
|
||||
(let [out (th/command! {::th/type :get-comment-threads
|
||||
::rpc/profile-id (:id outsider)
|
||||
:file-id (:id file)
|
||||
:share-id share-id})]
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= :not-found (th/ex-type (:error out))))))
|
||||
|
||||
(t/testing "outsider with who-comment=team share-link cannot create-comment-thread"
|
||||
(let [out (th/command! {::th/type :create-comment-thread
|
||||
::rpc/profile-id (:id outsider)
|
||||
:file-id (:id file)
|
||||
:page-id page-id
|
||||
:position (gpt/point 0)
|
||||
:content "outsider comment"
|
||||
:frame-id uuid/zero
|
||||
:share-id share-id})]
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= :not-found (th/ex-type (:error out))))))))
|
||||
|
||||
(t/deftest share-link-who-comment-all-can-comment
|
||||
(let [owner (th/create-profile* 1 {:is-active true})
|
||||
outsider (th/create-profile* 2 {:is-active true})
|
||||
|
||||
team (th/create-team* 1 {:profile-id (:id owner)})
|
||||
project (th/create-project* 1 {:team-id (:id team)
|
||||
:profile-id (:id owner)})
|
||||
file (th/create-file* 1 {:profile-id (:id owner)
|
||||
:project-id (:id project)})
|
||||
page-id (get-in file [:data :pages 0])
|
||||
|
||||
share (th/command! {::th/type :create-share-link
|
||||
::rpc/profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:pages #{page-id}
|
||||
:who-comment "all"
|
||||
:who-inspect "all"})
|
||||
share-id (get-in share [:result :id])]
|
||||
|
||||
(t/testing "outsider with who-comment=all share-link can get-comment-threads"
|
||||
(let [out (th/command! {::th/type :get-comment-threads
|
||||
::rpc/profile-id (:id outsider)
|
||||
:file-id (:id file)
|
||||
:share-id share-id})]
|
||||
(t/is (th/success? out))))
|
||||
|
||||
(t/testing "outsider with who-comment=all share-link can create-comment-thread"
|
||||
(let [out (th/command! {::th/type :create-comment-thread
|
||||
::rpc/profile-id (:id outsider)
|
||||
:file-id (:id file)
|
||||
:page-id page-id
|
||||
:position (gpt/point 0)
|
||||
:content "outsider comment"
|
||||
:frame-id uuid/zero
|
||||
:share-id share-id})]
|
||||
(t/is (th/success? out))))))
|
||||
|
||||
(t/deftest share-link-page-scope-enforced
|
||||
(let [owner (th/create-profile* 1 {:is-active true})
|
||||
outsider (th/create-profile* 2 {:is-active true})
|
||||
|
||||
team (th/create-team* 1 {:profile-id (:id owner)})
|
||||
project (th/create-project* 1 {:team-id (:id team)
|
||||
:profile-id (:id owner)})
|
||||
file (th/create-file* 1 {:profile-id (:id owner)
|
||||
:project-id (:id project)})
|
||||
|
||||
page-a (get-in file [:data :pages 0])
|
||||
page-b (uuid/random)
|
||||
|
||||
_ (th/command! {::th/type :update-file
|
||||
::rpc/profile-id (:id owner)
|
||||
:id (:id file)
|
||||
:session-id (uuid/random)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes [{:type :add-page
|
||||
:id page-b
|
||||
:page {:id page-b
|
||||
:name "Page B"
|
||||
:options {}
|
||||
:objects {}}}]})
|
||||
|
||||
thread-a (th/command! {::th/type :create-comment-thread
|
||||
::rpc/profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:page-id page-a
|
||||
:position (gpt/point 0)
|
||||
:content "comment on page A"
|
||||
:frame-id uuid/zero})
|
||||
thread-b (th/command! {::th/type :create-comment-thread
|
||||
::rpc/profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:page-id page-b
|
||||
:position (gpt/point 0)
|
||||
:content "comment on page B"
|
||||
:frame-id uuid/zero})
|
||||
|
||||
thread-a-id (get-in thread-a [:result :id])
|
||||
thread-b-id (get-in thread-b [:result :id])
|
||||
|
||||
share (th/command! {::th/type :create-share-link
|
||||
::rpc/profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:pages #{page-a}
|
||||
:who-comment "all"
|
||||
:who-inspect "all"})
|
||||
share-id (get-in share [:result :id])]
|
||||
|
||||
(t/testing "share-link holder can get-comment-threads for shared page only"
|
||||
(let [out (th/command! {::th/type :get-comment-threads
|
||||
::rpc/profile-id (:id outsider)
|
||||
:file-id (:id file)
|
||||
:share-id share-id})
|
||||
result (:result out)]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 1 (count result)))
|
||||
(t/is (= page-a (:page-id (first result))))))
|
||||
|
||||
(t/testing "share-link holder cannot get-comment-thread for unshared page"
|
||||
(let [out (th/command! {::th/type :get-comment-thread
|
||||
::rpc/profile-id (:id outsider)
|
||||
:file-id (:id file)
|
||||
:id thread-b-id
|
||||
:share-id share-id})]
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= :not-found (th/ex-type (:error out))))))
|
||||
|
||||
(t/testing "share-link holder can get-comment-thread for shared page"
|
||||
(let [out (th/command! {::th/type :get-comment-thread
|
||||
::rpc/profile-id (:id outsider)
|
||||
:file-id (:id file)
|
||||
:id thread-a-id
|
||||
:share-id share-id})]
|
||||
(t/is (th/success? out))))
|
||||
|
||||
(t/testing "share-link holder cannot get-comments for thread on unshared page"
|
||||
(let [out (th/command! {::th/type :get-comments
|
||||
::rpc/profile-id (:id outsider)
|
||||
:thread-id thread-b-id
|
||||
:share-id share-id})]
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= :not-found (th/ex-type (:error out))))))))
|
||||
|
||||
(t/deftest membership-can-still-comment
|
||||
(let [owner (th/create-profile* 1 {:is-active true})
|
||||
member (th/create-profile* 2 {:is-active true})
|
||||
|
||||
team (th/create-team* 1 {:profile-id (:id owner)})
|
||||
_ (th/create-team-role* {:team-id (:id team)
|
||||
:profile-id (:id member)
|
||||
:role :editor})
|
||||
project (th/create-project* 1 {:team-id (:id team)
|
||||
:profile-id (:id owner)})
|
||||
file (th/create-file* 1 {:profile-id (:id owner)
|
||||
:project-id (:id project)})
|
||||
page-id (get-in file [:data :pages 0])]
|
||||
|
||||
(t/testing "team member can get-comment-threads without share-id"
|
||||
(let [out (th/command! {::th/type :get-comment-threads
|
||||
::rpc/profile-id (:id member)
|
||||
:file-id (:id file)})]
|
||||
(t/is (th/success? out))))
|
||||
|
||||
(t/testing "team member can create-comment-thread without share-id"
|
||||
(let [out (th/command! {::th/type :create-comment-thread
|
||||
::rpc/profile-id (:id member)
|
||||
:file-id (:id file)
|
||||
:page-id page-id
|
||||
:position (gpt/point 0)
|
||||
:content "member comment"
|
||||
:frame-id uuid/zero})]
|
||||
(t/is (th/success? out))))))
|
||||
|
||||
40
backend/test/backend_tests/rpc_demo_test.clj
Normal file
40
backend/test/backend_tests/rpc_demo_test.clj
Normal file
@ -0,0 +1,40 @@
|
||||
;; 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 INC Sucursal en España SL
|
||||
|
||||
(ns backend-tests.rpc-demo-test
|
||||
(:require
|
||||
[app.auth :as auth]
|
||||
[app.config :as cf]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
;; Capture the real verifier before the shared test fixture replaces it.
|
||||
(def verify-password* auth/verify-password)
|
||||
|
||||
(t/deftest weak-password-hash-verifies
|
||||
(let [password "DemoPassword123!"
|
||||
hashed (auth/derive-password-weak password)]
|
||||
(t/is (:valid (verify-password* password hashed)))))
|
||||
|
||||
(t/deftest create-demo-profile-uses-unique-uuid-email
|
||||
(with-redefs [cf/flags (conj cf/flags :demo-users)]
|
||||
(let [first-result (th/command! {::th/type :create-demo-profile})
|
||||
second-result (th/command! {::th/type :create-demo-profile})
|
||||
first-profile (:result first-result)
|
||||
second-profile (:result second-result)]
|
||||
(t/is (nil? (:error first-result)))
|
||||
(t/is (nil? (:error second-result)))
|
||||
(t/is (re-matches #"demo-[0-9a-fA-F-]+@demo\.example\.com"
|
||||
(:email first-profile)))
|
||||
(t/is (not= (:email first-profile) (:email second-profile))))))
|
||||
|
||||
(t/deftest create-demo-profile-requires-feature-flag
|
||||
(with-redefs [cf/flags (disj cf/flags :demo-users)]
|
||||
(let [{:keys [error]} (th/command! {::th/type :create-demo-profile})]
|
||||
(t/is (th/ex-of-code? error :demo-users-not-allowed)))))
|
||||
@ -59,8 +59,13 @@
|
||||
(let [out (th/command! {::th/type :create-upload-session
|
||||
::rpc/profile-id (:id prof)
|
||||
:total-chunks total-chunks})]
|
||||
(t/is (nil? (:error out)))
|
||||
(:session-id (:result out))))
|
||||
(let [session-id (:session-id (:result out))]
|
||||
(t/is (nil? (:error out))
|
||||
(str "create-upload-session failed: "
|
||||
(some-> (:error out) ex-data)))
|
||||
(t/is (uuid? session-id)
|
||||
(str "create-upload-session returned an invalid session-id: " session-id))
|
||||
session-id)))
|
||||
|
||||
(defn- upload-font-chunked!
|
||||
"Splits `font-bytes` into chunks of `chunk-size` bytes, creates an upload
|
||||
@ -68,14 +73,15 @@
|
||||
[prof ^bytes font-bytes mtype chunk-size]
|
||||
(let [chunks (split-bytes-into-chunks font-bytes chunk-size)
|
||||
session-id (create-upload-session! prof (count chunks))]
|
||||
(doseq [[idx chunk-data] (map-indexed vector chunks)]
|
||||
(let [mfile (make-chunk-mfile chunk-data mtype)
|
||||
out (th/command! {::th/type :upload-chunk
|
||||
::rpc/profile-id (:id prof)
|
||||
:session-id session-id
|
||||
:index idx
|
||||
:content mfile})]
|
||||
(t/is (nil? (:error out)))))
|
||||
(when (uuid? session-id)
|
||||
(doseq [[idx chunk-data] (map-indexed vector chunks)]
|
||||
(let [mfile (make-chunk-mfile chunk-data mtype)
|
||||
out (th/command! {::th/type :upload-chunk
|
||||
::rpc/profile-id (:id prof)
|
||||
:session-id session-id
|
||||
:index idx
|
||||
:content mfile})]
|
||||
(t/is (nil? (:error out))))))
|
||||
session-id))
|
||||
|
||||
(defn- assert-font-variant-result
|
||||
@ -613,43 +619,42 @@
|
||||
;; N2-07: A user with edit permissions on their own team must not be
|
||||
;; able to create a font variant using a font-id that already belongs
|
||||
;; to another team (BOLA / CWE-639).
|
||||
(with-mocks [mock {:target 'app.rpc.quotes/check! :return nil}]
|
||||
(let [prof1 (th/create-profile* 1 {:is-active true})
|
||||
prof2 (th/create-profile* 2 {:is-active true})
|
||||
team1 (:default-team-id prof1)
|
||||
team2 (:default-team-id prof2)
|
||||
font-id (uuid/custom 10 999)
|
||||
data (-> (io/resource "backend_tests/test_files/font-1.ttf")
|
||||
(io/read*))]
|
||||
(let [prof1 (th/create-profile* 1 {:is-active true})
|
||||
prof2 (th/create-profile* 2 {:is-active true})
|
||||
team1 (:default-team-id prof1)
|
||||
team2 (:default-team-id prof2)
|
||||
font-id (uuid/custom 10 999)
|
||||
data (-> (io/resource "backend_tests/test_files/font-1.ttf")
|
||||
(io/read*))]
|
||||
|
||||
;; prof1 creates a font variant in team1 with font-id
|
||||
(let [session-id (upload-font-chunked! prof1 data "font/ttf" (* 4 1024 1024))
|
||||
params {::th/type :create-font-variant
|
||||
::rpc/profile-id (:id prof1)
|
||||
:team-id team1
|
||||
:font-id font-id
|
||||
:font-family "SharedFont"
|
||||
:font-weight 400
|
||||
:font-style "normal"
|
||||
:uploads {"font/ttf" session-id}}
|
||||
out (th/command! params)]
|
||||
(t/is (nil? (:error out))))
|
||||
;; prof1 creates a font variant in team1 with font-id
|
||||
(let [session-id (upload-font-chunked! prof1 data "font/ttf" (* 4 1024 1024))
|
||||
params {::th/type :create-font-variant
|
||||
::rpc/profile-id (:id prof1)
|
||||
:team-id team1
|
||||
:font-id font-id
|
||||
:font-family "SharedFont"
|
||||
:font-weight 400
|
||||
:font-style "normal"
|
||||
:uploads {"font/ttf" session-id}}
|
||||
out (th/command! params)]
|
||||
(t/is (nil? (:error out))))
|
||||
|
||||
;; prof2 tries to create a variant using the same font-id but
|
||||
;; in team2, which must be rejected because font-id belongs to team1
|
||||
(let [session-id (upload-font-chunked! prof2 data "font/ttf" (* 4 1024 1024))
|
||||
params {::th/type :create-font-variant
|
||||
::rpc/profile-id (:id prof2)
|
||||
:team-id team2
|
||||
:font-id font-id
|
||||
:font-family "SharedFont"
|
||||
:font-weight 700
|
||||
:font-style "normal"
|
||||
:uploads {"font/ttf" session-id}}
|
||||
out (th/command! params)]
|
||||
(t/is (some? (:error out)))
|
||||
(t/is (= :not-found (-> out :error ex-data :type)))
|
||||
(t/is (= :object-not-found (-> out :error ex-data :code)))))))
|
||||
;; prof2 tries to create a variant using the same font-id but
|
||||
;; in team2, which must be rejected because font-id belongs to team1
|
||||
(let [session-id (upload-font-chunked! prof2 data "font/ttf" (* 4 1024 1024))
|
||||
params {::th/type :create-font-variant
|
||||
::rpc/profile-id (:id prof2)
|
||||
:team-id team2
|
||||
:font-id font-id
|
||||
:font-family "SharedFont"
|
||||
:font-weight 700
|
||||
:font-style "normal"
|
||||
:uploads {"font/ttf" session-id}}
|
||||
out (th/command! params)]
|
||||
(t/is (some? (:error out)))
|
||||
(t/is (= :not-found (-> out :error ex-data :type)))
|
||||
(t/is (= :object-not-found (-> out :error ex-data :code))))))
|
||||
|
||||
(t/deftest get-font-variants-nonexistent-file
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
|
||||
@ -194,6 +194,15 @@
|
||||
(string? (get version k)))))
|
||||
(t/is (= cf/version version))))
|
||||
|
||||
(t/deftest get-air-gapped
|
||||
(let [out (th/management-command! {::th/type :get-air-gapped})]
|
||||
(t/is (th/success? out))
|
||||
(t/is (false? (-> out :result :air-gapped))))
|
||||
(binding [cf/flags (conj cf/flags :air-gapped-conf)]
|
||||
(let [out (th/management-command! {::th/type :get-air-gapped})]
|
||||
(t/is (th/success? out))
|
||||
(t/is (true? (-> out :result :air-gapped))))))
|
||||
|
||||
(t/deftest get-teams-returns-only-owned-non-default-non-deleted
|
||||
(with-mocks [nitrate-mock {:target 'app.nitrate/call :return nil}]
|
||||
(let [profile (th/create-profile* 1 {:is-active true})
|
||||
|
||||
@ -424,6 +424,38 @@
|
||||
(let [count-after (:count (th/db-exec-one! ["SELECT count(*) FROM http_session_v2 WHERE profile_id = ?" (:id prof)]))]
|
||||
(t/is (= 0 count-after)))))
|
||||
|
||||
(t/deftest profile-deletion-via-gc-cascades
|
||||
(let [prof (th/create-profile* 1)
|
||||
file (th/create-file* 1 {:profile-id (:id prof)
|
||||
:project-id (:default-project-id prof)
|
||||
:is-shared false})
|
||||
team-id (:default-team-id prof)
|
||||
project-id (:default-project-id prof)
|
||||
file-id (:id file)
|
||||
|
||||
deleted-at (ct/minus (ct/now) (ct/duration {:days 1}))]
|
||||
|
||||
(th/db-update! :profile
|
||||
{:deleted-at deleted-at}
|
||||
{:id (:id prof)})
|
||||
|
||||
(let [team-before (th/db-get :team {:id team-id} {::db/remove-deleted false})]
|
||||
(t/is (nil? (:deleted-at team-before))))
|
||||
|
||||
(let [result (th/run-task! :objects-gc {:min-age 0})]
|
||||
(t/is (pos? (:processed result))))
|
||||
|
||||
(let [profile-after (th/db-get :profile {:id (:id prof)} {::db/remove-deleted false})]
|
||||
(t/is (nil? profile-after)))
|
||||
|
||||
(let [team-after (th/db-get :team {:id team-id} {::db/remove-deleted false})]
|
||||
(t/is (nil? team-after)))
|
||||
|
||||
(let [project-after (th/db-get :project {:id project-id} {::db/remove-deleted false})]
|
||||
(t/is (nil? project-after)))
|
||||
|
||||
(let [file-after (th/db-get :file {:id file-id} {::db/remove-deleted false})]
|
||||
(t/is (nil? file-after)))))
|
||||
|
||||
(t/deftest email-blacklist-1
|
||||
(t/is (false? (email.blacklist/enabled? th/*system*)))
|
||||
|
||||
@ -1328,3 +1328,83 @@
|
||||
out (th/command! data)]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 1 (:call-count @mock)))))))
|
||||
|
||||
(t/deftest admin-cannot-remove-team-owner
|
||||
(let [owner (th/create-profile* 1 {:is-active true})
|
||||
admin (th/create-profile* 2 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id owner)})]
|
||||
|
||||
(th/create-team-role* {:team-id (:id team)
|
||||
:profile-id (:id admin)
|
||||
:role :admin})
|
||||
|
||||
(let [out (th/command! {::th/type :delete-team-member
|
||||
::rpc/profile-id (:id admin)
|
||||
:team-id (:id team)
|
||||
:member-id (:id owner)})]
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (th/ex-of-type? (:error out) :validation))
|
||||
(t/is (th/ex-of-code? (:error out) :cant-remove-owner)))))
|
||||
|
||||
(t/deftest owner-can-remove-another-owner
|
||||
(let [owner1 (th/create-profile* 1 {:is-active true})
|
||||
owner2 (th/create-profile* 2 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id owner1)})]
|
||||
|
||||
(th/create-team-role* {:team-id (:id team)
|
||||
:profile-id (:id owner2)
|
||||
:role :owner})
|
||||
|
||||
(let [out (th/command! {::th/type :delete-team-member
|
||||
::rpc/profile-id (:id owner1)
|
||||
:team-id (:id team)
|
||||
:member-id (:id owner2)})]
|
||||
(t/is (th/success? out)))))
|
||||
|
||||
(t/deftest owner-can-remove-admin
|
||||
(let [owner (th/create-profile* 1 {:is-active true})
|
||||
admin (th/create-profile* 2 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id owner)})]
|
||||
|
||||
(th/create-team-role* {:team-id (:id team)
|
||||
:profile-id (:id admin)
|
||||
:role :admin})
|
||||
|
||||
(let [out (th/command! {::th/type :delete-team-member
|
||||
::rpc/profile-id (:id owner)
|
||||
:team-id (:id team)
|
||||
:member-id (:id admin)})]
|
||||
(t/is (th/success? out)))))
|
||||
|
||||
(t/deftest admin-can-remove-admin
|
||||
(let [owner (th/create-profile* 1 {:is-active true})
|
||||
admin1 (th/create-profile* 2 {:is-active true})
|
||||
admin2 (th/create-profile* 3 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id owner)})]
|
||||
|
||||
(th/create-team-role* {:team-id (:id team)
|
||||
:profile-id (:id admin1)
|
||||
:role :admin})
|
||||
|
||||
(th/create-team-role* {:team-id (:id team)
|
||||
:profile-id (:id admin2)
|
||||
:role :admin})
|
||||
|
||||
(let [out (th/command! {::th/type :delete-team-member
|
||||
::rpc/profile-id (:id admin1)
|
||||
:team-id (:id team)
|
||||
:member-id (:id admin2)})]
|
||||
(t/is (th/success? out)))))
|
||||
|
||||
(t/deftest delete-nonexistent-member-returns-not-found
|
||||
(let [owner (th/create-profile* 1 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id owner)})
|
||||
fake-id (uuid/next)]
|
||||
|
||||
(let [out (th/command! {::th/type :delete-team-member
|
||||
::rpc/profile-id (:id owner)
|
||||
:team-id (:id team)
|
||||
:member-id fake-id})]
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (th/ex-of-type? (:error out) :not-found))
|
||||
(t/is (th/ex-of-code? (:error out) :member-does-not-exist)))))
|
||||
|
||||
@ -128,3 +128,80 @@
|
||||
(let [result (:result out)]
|
||||
(t/is (contains? result :file))
|
||||
(t/is (contains? result :project)))))))
|
||||
|
||||
(t/deftest share-link-token-disclosure
|
||||
(let [owner (th/create-profile* 1 {:is-active true})
|
||||
proj-id (:default-project-id owner)
|
||||
|
||||
file (th/create-file* 1 {:profile-id (:id owner)
|
||||
:project-id proj-id
|
||||
:is-shared false})
|
||||
|
||||
page-a (get-in file [:data :pages 0])
|
||||
page-b (uuid/random)
|
||||
|
||||
;; Add a second page to the file
|
||||
_ (th/command! {::th/type :update-file
|
||||
::rpc/profile-id (:id owner)
|
||||
:id (:id file)
|
||||
:session-id (uuid/random)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes [{:type :add-page
|
||||
:id page-b
|
||||
:page {:id page-b
|
||||
:name "Page B"
|
||||
:options {}
|
||||
:objects {}}}]})
|
||||
|
||||
;; Create Link A: restrictive (no pages, team-only comments/inspect)
|
||||
link-a (th/command! {::th/type :create-share-link
|
||||
::rpc/profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:pages #{}
|
||||
:who-comment "team"
|
||||
:who-inspect "team"})
|
||||
link-a-id (get-in link-a [:result :id])
|
||||
|
||||
;; Create Link B: permissive (all pages, all can comment/inspect)
|
||||
link-b (th/command! {::th/type :create-share-link
|
||||
::rpc/profile-id (:id owner)
|
||||
:file-id (:id file)
|
||||
:pages #{page-a page-b}
|
||||
:who-comment "all"
|
||||
:who-inspect "all"})
|
||||
link-b-id (get-in link-b [:result :id])]
|
||||
|
||||
(t/testing "restrictive share-link holder cannot see other share-link tokens"
|
||||
(let [out (th/command! {::th/type :get-view-only-bundle
|
||||
:share-id link-a-id
|
||||
:file-id (:id file)})
|
||||
err (:error out)
|
||||
result (:result out)
|
||||
share-links (:share-links result)]
|
||||
|
||||
;; Should not error
|
||||
(t/is (nil? err))
|
||||
|
||||
;; Should only see the share-link used for authentication
|
||||
(t/is (= 1 (count share-links)))
|
||||
(t/is (= link-a-id (:id (first share-links))))
|
||||
|
||||
;; Should NOT see Link B's token
|
||||
(t/is (not (some #(= link-b-id (:id %)) share-links)))))
|
||||
|
||||
(t/testing "team member still sees all share-links"
|
||||
(let [out (th/command! {::th/type :get-view-only-bundle
|
||||
::rpc/profile-id (:id owner)
|
||||
:file-id (:id file)})
|
||||
err (:error out)
|
||||
result (:result out)
|
||||
share-links (:share-links result)]
|
||||
|
||||
;; Should not error
|
||||
(t/is (nil? err))
|
||||
|
||||
;; Team member should see both share-links
|
||||
(t/is (= 2 (count share-links)))
|
||||
(t/is (some #(= link-a-id (:id %)) share-links))
|
||||
(t/is (some #(= link-b-id (:id %)) share-links))))))
|
||||
|
||||
@ -80,6 +80,24 @@
|
||||
(t/is (false? (ssrf/safe-url? "http://[fd00::1]/foo")))
|
||||
(t/is (false? (ssrf/safe-url? "http://[fc00::1]/foo"))))
|
||||
|
||||
(t/deftest validate-url-blocks-nat64-encoded-metadata
|
||||
;; 64:ff9b::a9fe:a9fe embeds 169.254.169.254 (cloud metadata)
|
||||
(t/is (false? (ssrf/safe-url? "http://[64:ff9b::a9fe:a9fe]/latest/meta-data/"))))
|
||||
|
||||
(t/deftest validate-url-blocks-nat64-encoded-loopback
|
||||
;; 64:ff9b::7f00:0001 embeds 127.0.0.1
|
||||
(t/is (false? (ssrf/safe-url? "http://[64:ff9b::7f00:1]/foo"))))
|
||||
|
||||
(t/deftest validate-url-blocks-6to4-encoded-private
|
||||
;; 2002:a00:1:: embeds 10.0.0.1; 2002:c0a8:101:: embeds 192.168.1.1
|
||||
(t/is (false? (ssrf/safe-url? "http://[2002:a00:1::1]/foo")))
|
||||
(t/is (false? (ssrf/safe-url? "http://[2002:c0a8:101::1]/foo"))))
|
||||
|
||||
(t/deftest validate-url-blocks-teredo-encoded-addresses
|
||||
;; Teredo server prefix 2001:0000::/32
|
||||
(t/is (false? (ssrf/safe-url?
|
||||
"http://[2001:0000:4136:e378:8000:63bf:3fff:fdd2]/foo"))))
|
||||
|
||||
(t/deftest validate-url-blocks-encoded-loopback
|
||||
;; Decimal encoding of 127.0.0.1 = 2130706433
|
||||
;; InetAddress normalizes this to 127.0.0.1
|
||||
|
||||
@ -17,14 +17,14 @@
|
||||
org.slf4j/slf4j-api {:mvn/version "2.0.18"}
|
||||
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.43"}
|
||||
|
||||
selmer/selmer {:mvn/version "1.13.4"}
|
||||
selmer/selmer {:mvn/version "1.13.5"}
|
||||
criterium/criterium {:mvn/version "0.4.6"}
|
||||
|
||||
metosin/jsonista {:mvn/version "1.0.0"
|
||||
:exclusions [com.fasterxml.jackson.core/jackson-core
|
||||
com.fasterxml.jackson.core/jackson-databind]}
|
||||
com.fasterxml.jackson.core/jackson-core {:mvn/version "2.22.1"}
|
||||
com.fasterxml.jackson.core/jackson-databind {:mvn/version "2.22.1"}
|
||||
com.fasterxml.jackson.core/jackson-core {:mvn/version "2.22.2"}
|
||||
com.fasterxml.jackson.core/jackson-databind {:mvn/version "2.22.2"}
|
||||
|
||||
|
||||
metosin/malli {:mvn/version "0.20.1"}
|
||||
@ -60,7 +60,7 @@
|
||||
{:dev
|
||||
{:extra-deps
|
||||
{org.clojure/tools.namespace {:mvn/version "1.5.1"}
|
||||
thheller/shadow-cljs {:mvn/version "3.4.11"}
|
||||
thheller/shadow-cljs {:mvn/version "3.5.0"}
|
||||
com.clojure-goes-fast/clj-async-profiler {:mvn/version "2.0.0-beta1"}
|
||||
com.bhauman/rebel-readline {:mvn/version "0.1.11"}
|
||||
criterium/criterium {:mvn/version "0.4.6"}
|
||||
|
||||
@ -4,18 +4,18 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC Sucursal en España SL",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
||||
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^10.0.4",
|
||||
"concurrently": "^10.0.5",
|
||||
"nodemon": "^3.1.14",
|
||||
"prettier": "3.9.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ws": "^8.21.2"
|
||||
"ws": "^8.21.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"date-fns": "^4.4.0"
|
||||
|
||||
121
common/pnpm-lock.yaml
generated
121
common/pnpm-lock.yaml
generated
@ -1,3 +1,104 @@
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
configDependencies: {}
|
||||
packageManagerDependencies:
|
||||
pnpm:
|
||||
specifier: 12.0.0
|
||||
version: 12.0.0
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.0.0':
|
||||
resolution: {integrity: sha512-sqeoPfVMIfQhbwzDrKraXY2ynyuWClFqzvfImzAS/yczEru1m5SGvQ9kgFPDvQzJZ9AetedgJeDZC6qYvH8/tQ==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.0.0':
|
||||
resolution: {integrity: sha512-Quc3J6c9cGTy+LDgz1cLVgCNOU9IERuyAlDoEj0DCilKqvo50Jx1GV8k74iwn4J9fFSKkm8JrwNvtTDj3uWnUA==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.0.0':
|
||||
resolution: {integrity: sha512-EVWd3OTmgsMFhXx69b5JxIzoabG9Ma7m4OeTaf0ZKBzMnfYi8u21NDQo92ToMrdYL5dYDDCHsyYIjXzk+d0HhA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.0.0':
|
||||
resolution: {integrity: sha512-cXHHW8M4rAPsYNkKZO9WVcpLLK55i9EaIsZPfIqUuY2eopd5LqnFyBge54HCh1GC0yCX8ySn0hYIi+4OyAEoDg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.0.0':
|
||||
resolution: {integrity: sha512-UcXwMdFjly0mpddkGigHKTxe27IMv2fUK4IWW/MHmJ3yMguxXmkwNlEI4aE+G1HO2TLo20uNEUWD4ymLe/DaCQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-x64@12.0.0':
|
||||
resolution: {integrity: sha512-6Rsl+zEWMOmus7v7/9J3OE8EMvHyNAfxYmDfmhQG4J0985OuT3G3Ho9NSGHjkBn4aU4bgklWifRhe1HX8dUSyw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.0.0':
|
||||
resolution: {integrity: sha512-O5F76A4oVFrpDGdFxEszRIThOSBfjHdH5c006gR+7UTCfiXrukr1XfqPungUI1DXcSR5gb9jBsPQqQZOAoOOxw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@pnpm/exe.win32-x64@12.0.0':
|
||||
resolution: {integrity: sha512-5dKFajIEWJ1ai+KHXFJvskY6vchbunmLwSUV2ywbLymcmJjfY5XJVpgzPCyIoVCMVG0zHorr66+hM8h8b3aRfQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
pnpm@12.0.0:
|
||||
resolution: {integrity: sha512-ni49w5EZlYaNyUuBdcIXwn6VQI+gO0oidJd48rNPdzt3zdOznt6BcbIvzVO+ajU0Lp+smUimjvWN9kiM6Jp+Zw==}
|
||||
engines: {node: '>=18.*'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-x64@12.0.0':
|
||||
optional: true
|
||||
|
||||
pnpm@12.0.0:
|
||||
optionalDependencies:
|
||||
'@pnpm/exe.darwin-arm64': 12.0.0
|
||||
'@pnpm/exe.darwin-x64': 12.0.0
|
||||
'@pnpm/exe.linux-arm64': 12.0.0
|
||||
'@pnpm/exe.linux-arm64-musl': 12.0.0
|
||||
'@pnpm/exe.linux-x64': 12.0.0
|
||||
'@pnpm/exe.linux-x64-musl': 12.0.0
|
||||
'@pnpm/exe.win32-arm64': 12.0.0
|
||||
'@pnpm/exe.win32-x64': 12.0.0
|
||||
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
@ -13,8 +114,8 @@ importers:
|
||||
version: 4.4.0
|
||||
devDependencies:
|
||||
concurrently:
|
||||
specifier: ^10.0.4
|
||||
version: 10.0.4
|
||||
specifier: ^10.0.5
|
||||
version: 10.0.5
|
||||
nodemon:
|
||||
specifier: ^3.1.14
|
||||
version: 3.1.14
|
||||
@ -25,8 +126,8 @@ importers:
|
||||
specifier: ^0.5.21
|
||||
version: 0.5.21
|
||||
ws:
|
||||
specifier: ^8.21.2
|
||||
version: 8.21.2
|
||||
specifier: ^8.21.3
|
||||
version: 8.21.3
|
||||
|
||||
packages:
|
||||
|
||||
@ -73,8 +174,8 @@ packages:
|
||||
resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
concurrently@10.0.4:
|
||||
resolution: {integrity: sha512-trZql+7l/0+WRAsAnEdctr4+iiOS6ZrViI6H8QWcCF9MFS/LT0dKpe8vluB1to6it+OxSI4VospFTIFMW8DJRw==}
|
||||
concurrently@10.0.5:
|
||||
resolution: {integrity: sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==}
|
||||
engines: {node: '>=22'}
|
||||
hasBin: true
|
||||
|
||||
@ -234,8 +335,8 @@ packages:
|
||||
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
ws@8.21.2:
|
||||
resolution: {integrity: sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==}
|
||||
ws@8.21.3:
|
||||
resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
@ -303,7 +404,7 @@ snapshots:
|
||||
strip-ansi: 7.2.0
|
||||
wrap-ansi: 9.0.2
|
||||
|
||||
concurrently@10.0.4:
|
||||
concurrently@10.0.5:
|
||||
dependencies:
|
||||
chalk: 5.6.2
|
||||
rxjs: 7.8.2
|
||||
@ -439,7 +540,7 @@ snapshots:
|
||||
string-width: 7.2.0
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
ws@8.21.2: {}
|
||||
ws@8.21.3: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
|
||||
@ -66,7 +66,7 @@ RUN set -ex; \
|
||||
|
||||
FROM base AS setup-node
|
||||
|
||||
ENV NODE_VERSION=v24.19.0 \
|
||||
ENV NODE_VERSION=v24.20.0 \
|
||||
PATH=/opt/node/bin:$PATH
|
||||
|
||||
RUN set -eux; \
|
||||
|
||||
10
docker/devenv/docker-compose.opencode.yml
Normal file
10
docker/devenv/docker-compose.opencode.yml
Normal file
@ -0,0 +1,10 @@
|
||||
# Optional compose overlay, included by manage.sh's instance-compose ONLY
|
||||
# when PENPOT_OPENCODE_CONFIG_DIR is set (run-devenv --opencode-config-dir
|
||||
# DIR). Bind-mounts a host directory over the container's opencode global
|
||||
# config dir (~/.config/opencode) so personal agents/prompts/skills kept in
|
||||
# a separate repo are available inside the devenv without committing them
|
||||
# here. Without the flag this file is never referenced.
|
||||
services:
|
||||
main:
|
||||
volumes:
|
||||
- "${PENPOT_OPENCODE_CONFIG_DIR}:/home/penpot/.config/opencode:z"
|
||||
@ -1,4 +1,4 @@
|
||||
FROM dhi.io/node:24.19.0-debian13-dev
|
||||
FROM dhi.io/node:24.20.0-debian13-dev
|
||||
LABEL maintainer="Penpot <docker@penpot.app>"
|
||||
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM dhi.io/node:24.18.1-debian13-dev AS build
|
||||
FROM dhi.io/node:24.20.0-debian13-dev AS build
|
||||
LABEL maintainer="Penpot <docker@penpot.app>"
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
@ -3,7 +3,7 @@ LABEL maintainer="Penpot <docker@penpot.app>"
|
||||
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8 \
|
||||
NODE_VERSION=v24.18.0 \
|
||||
NODE_VERSION=v24.20.0 \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
PATH=/opt/node/bin:$PATH
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
"@11ty/eleventy-plugin-rss": "^3.0.0",
|
||||
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
|
||||
"@tigersway/eleventy-plugin-ancestry": "^1.0.3",
|
||||
"@types/markdown-it": "14.1.2",
|
||||
"@types/markdown-it": "14.2.0",
|
||||
"elasticlunr": "^0.9.5",
|
||||
"eleventy-plugin-metagen": "^1.8.4",
|
||||
"eleventy-plugin-nesting-toc": "^1.3.0",
|
||||
@ -39,5 +39,5 @@
|
||||
"markdown-it-anchor": "^9.2.1",
|
||||
"markdown-it-plantuml": "^1.4.1"
|
||||
},
|
||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee"
|
||||
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67"
|
||||
}
|
||||
|
||||
235
docs/pnpm-lock.yaml
generated
235
docs/pnpm-lock.yaml
generated
@ -1,3 +1,104 @@
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
configDependencies: {}
|
||||
packageManagerDependencies:
|
||||
pnpm:
|
||||
specifier: 12.0.0
|
||||
version: 12.0.0
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.0.0':
|
||||
resolution: {integrity: sha512-sqeoPfVMIfQhbwzDrKraXY2ynyuWClFqzvfImzAS/yczEru1m5SGvQ9kgFPDvQzJZ9AetedgJeDZC6qYvH8/tQ==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.0.0':
|
||||
resolution: {integrity: sha512-Quc3J6c9cGTy+LDgz1cLVgCNOU9IERuyAlDoEj0DCilKqvo50Jx1GV8k74iwn4J9fFSKkm8JrwNvtTDj3uWnUA==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.0.0':
|
||||
resolution: {integrity: sha512-EVWd3OTmgsMFhXx69b5JxIzoabG9Ma7m4OeTaf0ZKBzMnfYi8u21NDQo92ToMrdYL5dYDDCHsyYIjXzk+d0HhA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.0.0':
|
||||
resolution: {integrity: sha512-cXHHW8M4rAPsYNkKZO9WVcpLLK55i9EaIsZPfIqUuY2eopd5LqnFyBge54HCh1GC0yCX8ySn0hYIi+4OyAEoDg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.0.0':
|
||||
resolution: {integrity: sha512-UcXwMdFjly0mpddkGigHKTxe27IMv2fUK4IWW/MHmJ3yMguxXmkwNlEI4aE+G1HO2TLo20uNEUWD4ymLe/DaCQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-x64@12.0.0':
|
||||
resolution: {integrity: sha512-6Rsl+zEWMOmus7v7/9J3OE8EMvHyNAfxYmDfmhQG4J0985OuT3G3Ho9NSGHjkBn4aU4bgklWifRhe1HX8dUSyw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.0.0':
|
||||
resolution: {integrity: sha512-O5F76A4oVFrpDGdFxEszRIThOSBfjHdH5c006gR+7UTCfiXrukr1XfqPungUI1DXcSR5gb9jBsPQqQZOAoOOxw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@pnpm/exe.win32-x64@12.0.0':
|
||||
resolution: {integrity: sha512-5dKFajIEWJ1ai+KHXFJvskY6vchbunmLwSUV2ywbLymcmJjfY5XJVpgzPCyIoVCMVG0zHorr66+hM8h8b3aRfQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
pnpm@12.0.0:
|
||||
resolution: {integrity: sha512-ni49w5EZlYaNyUuBdcIXwn6VQI+gO0oidJd48rNPdzt3zdOznt6BcbIvzVO+ajU0Lp+smUimjvWN9kiM6Jp+Zw==}
|
||||
engines: {node: '>=18.*'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-x64@12.0.0':
|
||||
optional: true
|
||||
|
||||
pnpm@12.0.0:
|
||||
optionalDependencies:
|
||||
'@pnpm/exe.darwin-arm64': 12.0.0
|
||||
'@pnpm/exe.darwin-x64': 12.0.0
|
||||
'@pnpm/exe.linux-arm64': 12.0.0
|
||||
'@pnpm/exe.linux-arm64-musl': 12.0.0
|
||||
'@pnpm/exe.linux-x64': 12.0.0
|
||||
'@pnpm/exe.linux-x64-musl': 12.0.0
|
||||
'@pnpm/exe.win32-arm64': 12.0.0
|
||||
'@pnpm/exe.win32-x64': 12.0.0
|
||||
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
@ -24,8 +125,8 @@ importers:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(@11ty/eleventy@3.1.6)
|
||||
'@types/markdown-it':
|
||||
specifier: 14.1.2
|
||||
version: 14.1.2
|
||||
specifier: 14.2.0
|
||||
version: 14.2.0
|
||||
elasticlunr:
|
||||
specifier: ^0.9.5
|
||||
version: 0.9.5
|
||||
@ -46,7 +147,7 @@ importers:
|
||||
version: 15.0.0
|
||||
markdown-it-anchor:
|
||||
specifier: ^9.2.1
|
||||
version: 9.2.1(@types/markdown-it@14.1.2)(markdown-it@15.0.0)
|
||||
version: 9.2.1(@types/markdown-it@14.2.0)(markdown-it@15.0.0)
|
||||
markdown-it-plantuml:
|
||||
specifier: ^1.4.1
|
||||
version: 1.4.1
|
||||
@ -64,8 +165,8 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@11ty/eleventy-fetch@5.1.2':
|
||||
resolution: {integrity: sha512-YxDARdR3S9UT4gOGRWgGNyokYT9jkCAjJge3OVKFdNMv1cyvWg1NHCvj9NVvK9XHenCLFy3cwvM/YYpZZTZopw==}
|
||||
'@11ty/eleventy-fetch@5.1.3':
|
||||
resolution: {integrity: sha512-4HS6QB/mVWTVlE6kjCKPkjkC1Z0YOqizJaHR05zK+E7a2b4EUC3T+wLktIy7wEl76ZoarkY45EKLmTw1XuR8fQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@11ty/eleventy-navigation@1.0.5':
|
||||
@ -102,8 +203,8 @@ packages:
|
||||
resolution: {integrity: sha512-oI7m8pa7/IAU/3lqRU9vjBbs20iKFo7x+1K9kT3aVira6scc1X9MjBdgLCHzLJeJ7iB6wydioA+kr9/qPnvmlQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@rgrove/parse-xml@4.2.0':
|
||||
resolution: {integrity: sha512-UuBOt7BOsKVOkFXRe4Ypd/lADuNIfqJXv8GvHqtXaTYXPPKkj2nS2zPllVsrtRjcomDhIJVBnZwfmlI222WH8g==}
|
||||
'@rgrove/parse-xml@4.2.3':
|
||||
resolution: {integrity: sha512-Jhlb+0zYez1T1yXUQs3F1qAtFuJljBVNdy9TKmLDauAXkxsOXopKYOyQ5Wm6SvP3fycav0GviX4Y15WWhGetMw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
'@sindresorhus/slugify@2.2.1':
|
||||
@ -122,8 +223,8 @@ packages:
|
||||
'@types/linkify-it@5.0.0':
|
||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||
|
||||
'@types/markdown-it@14.1.2':
|
||||
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
|
||||
'@types/markdown-it@14.2.0':
|
||||
resolution: {integrity: sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==}
|
||||
|
||||
'@types/mdurl@2.0.0':
|
||||
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
|
||||
@ -135,8 +236,8 @@ packages:
|
||||
resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
acorn@8.17.0:
|
||||
resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
|
||||
acorn@8.18.0:
|
||||
resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
@ -150,8 +251,8 @@ packages:
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
argparse@3.0.0:
|
||||
resolution: {integrity: sha512-BOp5NMrHqKxmq/OLr+clzzrRxgOKSLkcjmkWuChp7Irqwn4s74WjOBPIgWfA/HMcBnVkZ5XEuf9uUqzlpfCQ6A==}
|
||||
argparse@3.0.1:
|
||||
resolution: {integrity: sha512-nM4mHF/KM1v59ZNKX7zfusQz5wUAxR511YG8Vo6TyiV4aqhu++rbJW4v04xsWhpSsHFj66flT8P7znVpyO20xQ==}
|
||||
|
||||
asap@2.0.6:
|
||||
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
||||
@ -165,8 +266,8 @@ packages:
|
||||
bcp-47-normalize@2.3.0:
|
||||
resolution: {integrity: sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==}
|
||||
|
||||
bcp-47@2.1.0:
|
||||
resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==}
|
||||
bcp-47@2.1.1:
|
||||
resolution: {integrity: sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==}
|
||||
|
||||
binary-extensions@2.3.0:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
@ -363,8 +464,8 @@ packages:
|
||||
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
flatted@3.4.2:
|
||||
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
|
||||
flatted@3.4.4:
|
||||
resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
|
||||
|
||||
fresh@2.0.0:
|
||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||
@ -439,12 +540,12 @@ packages:
|
||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
|
||||
iso-639-1@3.1.5:
|
||||
resolution: {integrity: sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA==}
|
||||
iso-639-1@3.1.6:
|
||||
resolution: {integrity: sha512-ZFar/L4ngX7wZh2QX+Fiftmuf0igWJsrJtfizrovWifF1gAWkfmRa5Z1m0LQZbm0hKCHRDYhLRSLFrSqNe4EJA==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
||||
js-yaml@3.15.0:
|
||||
resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==}
|
||||
js-yaml@3.15.1:
|
||||
resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==}
|
||||
hasBin: true
|
||||
|
||||
js-yaml@4.3.1:
|
||||
@ -469,8 +570,8 @@ packages:
|
||||
linkify-it@6.1.0:
|
||||
resolution: {integrity: sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==}
|
||||
|
||||
liquidjs@10.28.0:
|
||||
resolution: {integrity: sha512-b6tmBXYMQTuGPnM5vB0CuZMo5kvmKMtSB/gvUWP6RFn2pIB5s+bJkBfIyJnattkc5bgeAiflrZVptx3iaLLioQ==}
|
||||
liquidjs@10.29.0:
|
||||
resolution: {integrity: sha512-pCVOhs6FLAR8su3ItJ07diN26t6W5dHQRnmTMy8HPyTFuv1+oSCVJIGp5pGjfQyOZfh50KswvKtMTp6p4JEIdw==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
@ -598,8 +699,8 @@ packages:
|
||||
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
picomatch@4.0.4:
|
||||
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
||||
picomatch@4.0.7:
|
||||
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
please-upgrade-node@3.2.0:
|
||||
@ -634,8 +735,8 @@ packages:
|
||||
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
range-parser@1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
range-parser@1.3.0:
|
||||
resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
readdirp@3.6.0:
|
||||
@ -652,8 +753,8 @@ packages:
|
||||
semver-compare@1.0.0:
|
||||
resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==}
|
||||
|
||||
semver@7.8.4:
|
||||
resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==}
|
||||
semver@7.8.5:
|
||||
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
@ -729,8 +830,8 @@ packages:
|
||||
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
ws@8.21.0:
|
||||
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
|
||||
ws@8.21.3:
|
||||
resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
@ -746,7 +847,7 @@ snapshots:
|
||||
'@11ty/dependency-tree-esm@2.0.4':
|
||||
dependencies:
|
||||
'@11ty/eleventy-utils': 2.0.7
|
||||
acorn: 8.17.0
|
||||
acorn: 8.18.0
|
||||
dependency-graph: 1.0.0
|
||||
normalize-path: 3.0.0
|
||||
|
||||
@ -767,18 +868,18 @@ snapshots:
|
||||
send: 1.2.1
|
||||
ssri: 11.0.0
|
||||
urlpattern-polyfill: 10.1.0
|
||||
ws: 8.21.0
|
||||
ws: 8.21.3
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@11ty/eleventy-fetch@5.1.2':
|
||||
'@11ty/eleventy-fetch@5.1.3':
|
||||
dependencies:
|
||||
'@11ty/eleventy-utils': 2.0.7
|
||||
'@rgrove/parse-xml': 4.2.0
|
||||
'@rgrove/parse-xml': 4.2.3
|
||||
debug: 4.4.3
|
||||
flatted: 3.4.2
|
||||
flatted: 3.4.4
|
||||
p-queue: 6.6.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -829,21 +930,21 @@ snapshots:
|
||||
entities: 6.0.1
|
||||
filesize: 10.1.6
|
||||
gray-matter: 4.0.3
|
||||
iso-639-1: 3.1.5
|
||||
iso-639-1: 3.1.6
|
||||
js-yaml: 4.3.1
|
||||
kleur: 4.1.5
|
||||
liquidjs: 10.28.0
|
||||
liquidjs: 10.29.0
|
||||
luxon: 3.7.2
|
||||
markdown-it: 14.3.0
|
||||
minimist: 1.2.8
|
||||
moo: 0.5.2
|
||||
node-retrieve-globals: 6.0.1
|
||||
nunjucks: 3.2.4(chokidar@3.6.0)
|
||||
picomatch: 4.0.4
|
||||
picomatch: 4.0.7
|
||||
please-upgrade-node: 3.2.0
|
||||
posthtml: 0.16.7
|
||||
posthtml-match-helper: 2.0.3(posthtml@0.16.7)
|
||||
semver: 7.8.4
|
||||
semver: 7.8.5
|
||||
slugify: 1.6.9
|
||||
tinyglobby: 0.2.17
|
||||
transitivePeerDependencies:
|
||||
@ -867,7 +968,7 @@ snapshots:
|
||||
minimatch: 3.1.5
|
||||
slash: 3.0.0
|
||||
|
||||
'@rgrove/parse-xml@4.2.0': {}
|
||||
'@rgrove/parse-xml@4.2.3': {}
|
||||
|
||||
'@sindresorhus/slugify@2.2.1':
|
||||
dependencies:
|
||||
@ -884,7 +985,7 @@ snapshots:
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
'@types/markdown-it@14.1.2':
|
||||
'@types/markdown-it@14.2.0':
|
||||
dependencies:
|
||||
'@types/linkify-it': 5.0.0
|
||||
'@types/mdurl': 2.0.0
|
||||
@ -895,9 +996,9 @@ snapshots:
|
||||
|
||||
acorn-walk@8.3.5:
|
||||
dependencies:
|
||||
acorn: 8.17.0
|
||||
acorn: 8.18.0
|
||||
|
||||
acorn@8.17.0: {}
|
||||
acorn@8.18.0: {}
|
||||
|
||||
anymatch@3.1.3:
|
||||
dependencies:
|
||||
@ -910,7 +1011,7 @@ snapshots:
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
argparse@3.0.0: {}
|
||||
argparse@3.0.1: {}
|
||||
|
||||
asap@2.0.6: {}
|
||||
|
||||
@ -920,10 +1021,10 @@ snapshots:
|
||||
|
||||
bcp-47-normalize@2.3.0:
|
||||
dependencies:
|
||||
bcp-47: 2.1.0
|
||||
bcp-47: 2.1.1
|
||||
bcp-47-match: 2.0.3
|
||||
|
||||
bcp-47@2.1.0:
|
||||
bcp-47@2.1.1:
|
||||
dependencies:
|
||||
is-alphabetical: 2.0.1
|
||||
is-alphanumerical: 2.0.1
|
||||
@ -1060,7 +1161,7 @@ snapshots:
|
||||
|
||||
eleventy-plugin-youtube-embed@1.13.2:
|
||||
dependencies:
|
||||
'@11ty/eleventy-fetch': 5.1.2
|
||||
'@11ty/eleventy-fetch': 5.1.3
|
||||
deepmerge: 4.3.1
|
||||
lite-youtube-embed: 0.3.4
|
||||
string-replace-async: 3.0.2
|
||||
@ -1096,7 +1197,7 @@ snapshots:
|
||||
|
||||
esm-import-transformer@3.0.5:
|
||||
dependencies:
|
||||
acorn: 8.17.0
|
||||
acorn: 8.18.0
|
||||
|
||||
esprima@4.0.1: {}
|
||||
|
||||
@ -1110,9 +1211,9 @@ snapshots:
|
||||
dependencies:
|
||||
is-extendable: 0.1.1
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.4):
|
||||
fdir@6.5.0(picomatch@4.0.7):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.4
|
||||
picomatch: 4.0.7
|
||||
|
||||
filesize@10.1.6: {}
|
||||
|
||||
@ -1132,7 +1233,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
flatted@3.4.2: {}
|
||||
flatted@3.4.4: {}
|
||||
|
||||
fresh@2.0.0: {}
|
||||
|
||||
@ -1147,7 +1248,7 @@ snapshots:
|
||||
|
||||
gray-matter@4.0.3:
|
||||
dependencies:
|
||||
js-yaml: 3.15.0
|
||||
js-yaml: 3.15.1
|
||||
kind-of: 6.0.3
|
||||
section-matter: 1.0.0
|
||||
strip-bom-string: 1.0.0
|
||||
@ -1207,9 +1308,9 @@ snapshots:
|
||||
|
||||
is-number@7.0.0: {}
|
||||
|
||||
iso-639-1@3.1.5: {}
|
||||
iso-639-1@3.1.6: {}
|
||||
|
||||
js-yaml@3.15.0:
|
||||
js-yaml@3.15.1:
|
||||
dependencies:
|
||||
argparse: 1.0.10
|
||||
esprima: 4.0.1
|
||||
@ -1232,7 +1333,7 @@ snapshots:
|
||||
dependencies:
|
||||
uc.micro: 3.0.0
|
||||
|
||||
liquidjs@10.28.0:
|
||||
liquidjs@10.29.0:
|
||||
dependencies:
|
||||
commander: 10.0.1
|
||||
|
||||
@ -1242,9 +1343,9 @@ snapshots:
|
||||
|
||||
luxon@3.7.2: {}
|
||||
|
||||
markdown-it-anchor@9.2.1(@types/markdown-it@14.1.2)(markdown-it@15.0.0):
|
||||
markdown-it-anchor@9.2.1(@types/markdown-it@14.2.0)(markdown-it@15.0.0):
|
||||
dependencies:
|
||||
'@types/markdown-it': 14.1.2
|
||||
'@types/markdown-it': 14.2.0
|
||||
markdown-it: 15.0.0
|
||||
|
||||
markdown-it-plantuml@1.4.1: {}
|
||||
@ -1260,7 +1361,7 @@ snapshots:
|
||||
|
||||
markdown-it@15.0.0:
|
||||
dependencies:
|
||||
argparse: 3.0.0
|
||||
argparse: 3.0.1
|
||||
entities: 8.0.0
|
||||
linkify-it: 6.1.0
|
||||
mdurl: 2.1.0
|
||||
@ -1299,7 +1400,7 @@ snapshots:
|
||||
|
||||
node-retrieve-globals@6.0.1:
|
||||
dependencies:
|
||||
acorn: 8.17.0
|
||||
acorn: 8.18.0
|
||||
acorn-walk: 8.3.5
|
||||
esm-import-transformer: 3.0.5
|
||||
|
||||
@ -1351,7 +1452,7 @@ snapshots:
|
||||
|
||||
picomatch@2.3.2: {}
|
||||
|
||||
picomatch@4.0.4: {}
|
||||
picomatch@4.0.7: {}
|
||||
|
||||
please-upgrade-node@3.2.0:
|
||||
dependencies:
|
||||
@ -1380,7 +1481,7 @@ snapshots:
|
||||
|
||||
punycode.js@2.3.1: {}
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
range-parser@1.3.0: {}
|
||||
|
||||
readdirp@3.6.0:
|
||||
dependencies:
|
||||
@ -1395,7 +1496,7 @@ snapshots:
|
||||
|
||||
semver-compare@1.0.0: {}
|
||||
|
||||
semver@7.8.4: {}
|
||||
semver@7.8.5: {}
|
||||
|
||||
send@1.2.1:
|
||||
dependencies:
|
||||
@ -1408,7 +1509,7 @@ snapshots:
|
||||
mime-types: 3.0.2
|
||||
ms: 2.1.3
|
||||
on-finished: 2.4.1
|
||||
range-parser: 1.2.1
|
||||
range-parser: 1.3.0
|
||||
statuses: 2.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -1433,8 +1534,8 @@ snapshots:
|
||||
|
||||
tinyglobby@0.2.17:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
fdir: 6.5.0(picomatch@4.0.7)
|
||||
picomatch: 4.0.7
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
@ -1458,4 +1559,4 @@ snapshots:
|
||||
|
||||
whatwg-mimetype@4.0.0: {}
|
||||
|
||||
ws@8.21.0: {}
|
||||
ws@8.21.3: {}
|
||||
|
||||
@ -148,10 +148,15 @@ automatically, so regular users never run this.
|
||||
```bash
|
||||
./manage.sh run-devenv --agentic \
|
||||
[--ws N] [--sync] [--serena-context CTX] \
|
||||
[--opencode-config-dir DIR] \
|
||||
[--git-user-name NAME] [--git-user-email EMAIL]
|
||||
```
|
||||
|
||||
Brings one agentic instance up. Errors out if the target is already running.
|
||||
`--opencode-config-dir DIR` bind-mounts DIR over the container's
|
||||
`~/.config/opencode` so personal agents/prompts/skills kept in a separate
|
||||
repository are available to the coding agent; see the
|
||||
[Dev environment guide](./devenv.md#personal-opencode-config-inside-the-container).
|
||||
|
||||
`--ws N` (N ≥ 1) brings that workspace up independently — workspaces can be
|
||||
started and stopped in any order. Per-instance ports
|
||||
|
||||
@ -139,6 +139,27 @@ until you set an identity. The values are applied every time
|
||||
`run-devenv` brings an instance up (idempotent), so re-running
|
||||
with different flags is the way to change the in-container identity.
|
||||
|
||||
### Personal opencode config inside the container
|
||||
|
||||
`run-devenv --opencode-config-dir DIR` bind-mounts a host directory over the
|
||||
container's `~/.config/opencode` (opencode's global config dir). This is how
|
||||
you keep personal agents, prompts, and skills in a separate repository and
|
||||
use them inside the devenv without committing them here or leaving untracked
|
||||
files in the repo:
|
||||
|
||||
```bash
|
||||
./manage.sh run-devenv --agentic --opencode-config-dir ../penpot-opencode
|
||||
```
|
||||
|
||||
The path must be an existing directory; `~` is expanded and the value is
|
||||
resolved to an absolute path automatically. The mount is applied at container
|
||||
creation, so changing it requires stopping and re-running `run-devenv` for
|
||||
that instance, and it applies only to instances brought up with the flag —
|
||||
other workspaces mount nothing. The directory is shared read-write with the
|
||||
container (same UID mapping as the source tree). Opencode's own state
|
||||
(sessions, `auth.json`) lives in `~/.local/share/opencode`, which stays in
|
||||
the container's data volume regardless of this flag.
|
||||
|
||||
### Shared state and workers
|
||||
|
||||
All instances share one Penpot database and one MinIO bucket; users, teams,
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
:dev
|
||||
{:extra-deps
|
||||
{thheller/shadow-cljs {:mvn/version "3.4.11"}}}
|
||||
{thheller/shadow-cljs {:mvn/version "3.5.0"}}}
|
||||
|
||||
:shadow-cljs
|
||||
{:main-opts ["-m" "shadow.cljs.devtools.cli"]
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC Sucursal en España SL",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
||||
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
@ -21,12 +21,12 @@
|
||||
"playwright": "1.62.1",
|
||||
"raw-body": "^4.0.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"undici": "^8.9.0",
|
||||
"undici": "^8.10.0",
|
||||
"xml-js": "^1.6.11",
|
||||
"xregexp": "^5.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ws": "^8.21.1"
|
||||
"ws": "^8.21.3"
|
||||
},
|
||||
"scripts": {
|
||||
"clear:shadow-cache": "rm -rf .shadow-cljs && rm -rf target",
|
||||
|
||||
121
exporter/pnpm-lock.yaml
generated
121
exporter/pnpm-lock.yaml
generated
@ -1,3 +1,104 @@
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
configDependencies: {}
|
||||
packageManagerDependencies:
|
||||
pnpm:
|
||||
specifier: 12.0.0
|
||||
version: 12.0.0
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.0.0':
|
||||
resolution: {integrity: sha512-sqeoPfVMIfQhbwzDrKraXY2ynyuWClFqzvfImzAS/yczEru1m5SGvQ9kgFPDvQzJZ9AetedgJeDZC6qYvH8/tQ==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.0.0':
|
||||
resolution: {integrity: sha512-Quc3J6c9cGTy+LDgz1cLVgCNOU9IERuyAlDoEj0DCilKqvo50Jx1GV8k74iwn4J9fFSKkm8JrwNvtTDj3uWnUA==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.0.0':
|
||||
resolution: {integrity: sha512-EVWd3OTmgsMFhXx69b5JxIzoabG9Ma7m4OeTaf0ZKBzMnfYi8u21NDQo92ToMrdYL5dYDDCHsyYIjXzk+d0HhA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.0.0':
|
||||
resolution: {integrity: sha512-cXHHW8M4rAPsYNkKZO9WVcpLLK55i9EaIsZPfIqUuY2eopd5LqnFyBge54HCh1GC0yCX8ySn0hYIi+4OyAEoDg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.0.0':
|
||||
resolution: {integrity: sha512-UcXwMdFjly0mpddkGigHKTxe27IMv2fUK4IWW/MHmJ3yMguxXmkwNlEI4aE+G1HO2TLo20uNEUWD4ymLe/DaCQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-x64@12.0.0':
|
||||
resolution: {integrity: sha512-6Rsl+zEWMOmus7v7/9J3OE8EMvHyNAfxYmDfmhQG4J0985OuT3G3Ho9NSGHjkBn4aU4bgklWifRhe1HX8dUSyw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.0.0':
|
||||
resolution: {integrity: sha512-O5F76A4oVFrpDGdFxEszRIThOSBfjHdH5c006gR+7UTCfiXrukr1XfqPungUI1DXcSR5gb9jBsPQqQZOAoOOxw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@pnpm/exe.win32-x64@12.0.0':
|
||||
resolution: {integrity: sha512-5dKFajIEWJ1ai+KHXFJvskY6vchbunmLwSUV2ywbLymcmJjfY5XJVpgzPCyIoVCMVG0zHorr66+hM8h8b3aRfQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
pnpm@12.0.0:
|
||||
resolution: {integrity: sha512-ni49w5EZlYaNyUuBdcIXwn6VQI+gO0oidJd48rNPdzt3zdOznt6BcbIvzVO+ajU0Lp+smUimjvWN9kiM6Jp+Zw==}
|
||||
engines: {node: '>=18.*'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-x64@12.0.0':
|
||||
optional: true
|
||||
|
||||
pnpm@12.0.0:
|
||||
optionalDependencies:
|
||||
'@pnpm/exe.darwin-arm64': 12.0.0
|
||||
'@pnpm/exe.darwin-x64': 12.0.0
|
||||
'@pnpm/exe.linux-arm64': 12.0.0
|
||||
'@pnpm/exe.linux-arm64-musl': 12.0.0
|
||||
'@pnpm/exe.linux-x64': 12.0.0
|
||||
'@pnpm/exe.linux-x64-musl': 12.0.0
|
||||
'@pnpm/exe.win32-arm64': 12.0.0
|
||||
'@pnpm/exe.win32-x64': 12.0.0
|
||||
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
@ -45,8 +146,8 @@ importers:
|
||||
specifier: ^0.5.21
|
||||
version: 0.5.21
|
||||
undici:
|
||||
specifier: ^8.9.0
|
||||
version: 8.9.0
|
||||
specifier: ^8.10.0
|
||||
version: 8.10.0
|
||||
xml-js:
|
||||
specifier: ^1.6.11
|
||||
version: 1.6.11
|
||||
@ -55,8 +156,8 @@ importers:
|
||||
version: 5.1.2
|
||||
devDependencies:
|
||||
ws:
|
||||
specifier: ^8.21.1
|
||||
version: 8.21.1
|
||||
specifier: ^8.21.3
|
||||
version: 8.21.3
|
||||
|
||||
packages:
|
||||
|
||||
@ -420,15 +521,15 @@ packages:
|
||||
resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
|
||||
engines: {node: '>=0.6.x'}
|
||||
|
||||
undici@8.9.0:
|
||||
resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==}
|
||||
undici@8.10.0:
|
||||
resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
ws@8.21.1:
|
||||
resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==}
|
||||
ws@8.21.3:
|
||||
resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
@ -802,11 +903,11 @@ snapshots:
|
||||
|
||||
tsscmp@1.0.6: {}
|
||||
|
||||
undici@8.9.0: {}
|
||||
undici@8.10.0: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
ws@8.21.1: {}
|
||||
ws@8.21.3: {}
|
||||
|
||||
xml-js@1.6.11:
|
||||
dependencies:
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
penpot/common
|
||||
{:local/root "../common"}
|
||||
|
||||
org.clojure/clojure {:mvn/version "1.12.2"}
|
||||
org.clojure/clojure {:mvn/version "1.12.5"}
|
||||
binaryage/devtools {:mvn/version "RELEASE"}
|
||||
metosin/reitit-core {:mvn/version "0.10.1"}
|
||||
funcool/okulary {:mvn/version "2022.04.11-16"}
|
||||
@ -51,7 +51,7 @@
|
||||
"--enable-native-access=ALL-UNNAMED"]
|
||||
|
||||
:extra-deps
|
||||
{thheller/shadow-cljs {:mvn/version "3.4.11"}
|
||||
{thheller/shadow-cljs {:mvn/version "3.5.0"}
|
||||
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
|
||||
org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
||||
criterium/criterium {:mvn/version "0.4.6"}}}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC Sucursal en España SL",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
||||
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
],
|
||||
@ -59,33 +59,33 @@
|
||||
"@penpot/tokenscript": "link:packages/tokenscript",
|
||||
"@penpot/ua-parser": "penpot/ua-parser#1.0.0",
|
||||
"@playwright/test": "1.62.1",
|
||||
"@storybook/addon-docs": "10.5.6",
|
||||
"@storybook/addon-themes": "10.5.6",
|
||||
"@storybook/addon-vitest": "10.5.6",
|
||||
"@storybook/react-vite": "10.5.6",
|
||||
"@storybook/addon-docs": "10.5.10",
|
||||
"@storybook/addon-themes": "10.5.10",
|
||||
"@storybook/addon-vitest": "10.5.10",
|
||||
"@storybook/react-vite": "10.5.10",
|
||||
"@tokens-studio/sd-transforms": "2.0.3",
|
||||
"@types/node": "^26.1.2",
|
||||
"@vitest/browser": "4.1.10",
|
||||
"@vitest/browser-playwright": "4.1.10",
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
"@types/node": "^26.4.0",
|
||||
"@vitest/browser": "4.1.11",
|
||||
"@vitest/browser-playwright": "4.1.11",
|
||||
"@vitest/coverage-v8": "4.1.11",
|
||||
"@zip.js/zip.js": "2.8.34",
|
||||
"autoprefixer": "^10.5.4",
|
||||
"compression": "^1.8.1",
|
||||
"concurrently": "^10.0.4",
|
||||
"concurrently": "^10.0.5",
|
||||
"date-fns": "^4.4.0",
|
||||
"esbuild": "^0.28.1",
|
||||
"eventsource-parser": "^3.1.0",
|
||||
"esbuild": "^0.28.2",
|
||||
"eventsource-parser": "^4.1.0",
|
||||
"express": "^5.1.0",
|
||||
"fancy-log": "^2.0.0",
|
||||
"getopts": "^2.3.0",
|
||||
"gettext-parser": "^9.1.1",
|
||||
"highlight.js": "^11.10.0",
|
||||
"highlight.js": "^11.12.0",
|
||||
"js-beautify": "^2.0.3",
|
||||
"jsdom": "^30.0.1",
|
||||
"lodash": "^4.18.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"map-stream": "0.0.7",
|
||||
"marked": "^18.0.9",
|
||||
"marked": "^18.0.11",
|
||||
"mkdirp": "^3.0.1",
|
||||
"mustache": "^4.2.0",
|
||||
"nodemon": "^3.1.14",
|
||||
@ -93,7 +93,7 @@
|
||||
"opentype.js": "^2.0.0",
|
||||
"p-limit": "^7.3.1",
|
||||
"playwright": "1.62.1",
|
||||
"postcss": "^8.5.25",
|
||||
"postcss": "^8.5.26",
|
||||
"postcss-clean": "^1.2.2",
|
||||
"postcss-modules": "^9.0.1",
|
||||
"postcss-scss": "^4.0.9",
|
||||
@ -103,27 +103,27 @@
|
||||
"randomcolor": "^0.6.2",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8",
|
||||
"react-error-boundary": "^6.1.2",
|
||||
"react-error-boundary": "^6.1.3",
|
||||
"react-virtualized": "^9.22.6",
|
||||
"rimraf": "^6.1.3",
|
||||
"rxjs": "8.0.0-alpha.14",
|
||||
"sass": "^1.102.0",
|
||||
"sass-embedded": "^1.100.0",
|
||||
"sass": "^1.103.1",
|
||||
"sass-embedded": "^1.103.1",
|
||||
"sax": "^1.6.1",
|
||||
"scheduler": "^0.27.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"storybook": "10.5.6",
|
||||
"style-dictionary": "5.5.0",
|
||||
"storybook": "10.5.10",
|
||||
"style-dictionary": "5.5.2",
|
||||
"stylelint": "^17.14.1",
|
||||
"stylelint-config-standard-scss": "^17.0.0",
|
||||
"stylelint-plugin-logical-css": "^2.1.0",
|
||||
"stylelint-scss": "^7.2.0",
|
||||
"svg-sprite": "^2.0.4",
|
||||
"tdigest": "^0.1.2",
|
||||
"tdigest": "^0.1.3",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"typescript": "^6.0.2",
|
||||
"vite": "^8.2.0",
|
||||
"vitest": "^4.1.10",
|
||||
"vite": "^8.2.2",
|
||||
"vitest": "^4.1.11",
|
||||
"wait-on": "^9.1.0",
|
||||
"watcher": "^2.3.1",
|
||||
"workerpool": "^10.0.3",
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"description": "Penpot Draft-JS Wrapper",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
||||
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||
"author": "Andrey Antukh",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"description": "Simple library for handling keyboard shortcuts",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
||||
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||
"author": "Craig Campbell",
|
||||
"license": "Apache-2.0 WITH LLVM-exception"
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
||||
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||
"author": "Andrey Antukh",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
|
||||
@ -20,20 +20,20 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^8.0.1",
|
||||
"@babel/preset-react": "^8.0.1",
|
||||
"@storybook/react": "10.5.6",
|
||||
"@storybook/react-vite": "10.5.6",
|
||||
"@storybook/react": "10.5.10",
|
||||
"@storybook/react-vite": "10.5.10",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/react": "16.3.2",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"@vitejs/plugin-react": "^6.0.5",
|
||||
"@types/react-dom": "^19.2.5",
|
||||
"@vitejs/plugin-react": "^6.1.0",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "7.1.1",
|
||||
"react-compiler-runtime": "^1.0.0",
|
||||
"storybook": "10.5.6",
|
||||
"storybook": "10.5.10",
|
||||
"vite-plugin-dts": "^5.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
1608
frontend/pnpm-lock.yaml
generated
1608
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -13,7 +13,7 @@
|
||||
[app.main.ui.auth.login :refer [login-page*]]
|
||||
[app.main.ui.auth.recovery :refer [recovery-page*]]
|
||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page*]]
|
||||
[app.main.ui.auth.register :refer [register-page* register-success-page* register-validate-page* terms-register*]]
|
||||
[app.main.ui.auth.register :refer [register-page* register-success-page* register-validate-page* terms-service-privacy-policy*]]
|
||||
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*] :as raw-svg]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.util.dom :as dom]
|
||||
@ -74,7 +74,7 @@
|
||||
[:> recovery-page* {:params params}])
|
||||
|
||||
(when (= section :auth-register)
|
||||
[:> terms-register*])]]))
|
||||
[:> terms-service-privacy-policy*])]]))
|
||||
|
||||
|
||||
(mf/defc auth-page*
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "./ds/mixins.scss" as *;
|
||||
|
||||
.auth-section {
|
||||
display: grid;
|
||||
@ -26,6 +27,8 @@
|
||||
}
|
||||
|
||||
.auth-section.register {
|
||||
@include custom-scrollbar;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@ -4,56 +4,15 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
|
||||
.auth-form-wrapper {
|
||||
inline-size: 100%;
|
||||
padding-block-end: 0;
|
||||
display: grid;
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-m);
|
||||
|
||||
// Native <form> inside auth-form-wrapper — no class available
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-m);
|
||||
margin-block-start: var(--sp-m);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-title-wrapper {
|
||||
inline-size: 100%;
|
||||
padding-block-end: 0;
|
||||
display: grid;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-color: var(--color-background-quaternary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
@include use-typography("title-large");
|
||||
|
||||
line-height: 1.2;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.auth-subtitle {
|
||||
@include use-typography("title-small");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.auth-tagline {
|
||||
@include use-typography("title-small");
|
||||
|
||||
margin: 0;
|
||||
color: var(--color-foreground-secondary);
|
||||
margin: var(--sp-m) 0;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
@ -62,36 +21,40 @@
|
||||
--input-min-width: 100%;
|
||||
}
|
||||
|
||||
.buttons-stack {
|
||||
display: grid;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
.form-submit-btn {
|
||||
--button-bg-color: var(--color-accent-primary);
|
||||
--button-border-color: var(--color-accent-primary);
|
||||
--button-fg-color: var(--color-background-secondary);
|
||||
|
||||
.login-button,
|
||||
.login-ldap-button {
|
||||
@include use-typography("headline-small");
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-accent-primary);
|
||||
border: $b-1 solid var(--color-accent-primary);
|
||||
color: var(--color-background-secondary);
|
||||
background-color: var(--button-bg-color);
|
||||
border: $b-1 solid var(--button-border-color);
|
||||
color: var(--button-fg-color);
|
||||
border-radius: $br-8;
|
||||
min-block-size: $sz-32;
|
||||
block-size: $sz-40;
|
||||
inline-size: 100%;
|
||||
|
||||
&:hover {
|
||||
--button-bg-color: var(--color-accent-tertiary);
|
||||
--button-border-color: var(--color-accent-tertiary);
|
||||
--button-fg-color: var(--color-background-secondary);
|
||||
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--color-background-quaternary);
|
||||
border: 1px solid var(--color-background-quaternary);
|
||||
color: var(--color-foreground-disabled);
|
||||
cursor: unset;
|
||||
--button-bg-color: var(--color-background-quaternary);
|
||||
--button-border-color: var(--color-background-quaternary);
|
||||
--button-fg-color: var(--color-foreground-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.go-back {
|
||||
.go-back-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-m);
|
||||
@ -100,19 +63,68 @@
|
||||
}
|
||||
|
||||
.go-back-link {
|
||||
--button-bg-color: var(--color-background-tertiary);
|
||||
--button-border-color: var(--color-background-tertiary);
|
||||
--button-fg-color: var(--color-foreground-secondary);
|
||||
|
||||
@include use-typography("headline-small");
|
||||
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-tertiary);
|
||||
border: $b-1 solid var(--color-background-tertiary);
|
||||
color: var(--color-foreground-secondary);
|
||||
|
||||
@include use-typography("headline-small");
|
||||
|
||||
background-color: var(--button-bg-color);
|
||||
border: $b-1 solid var(--button-border-color);
|
||||
color: var(--button-fg-color);
|
||||
block-size: $sz-40;
|
||||
|
||||
&:hover {
|
||||
--button-bg-color: var(--color-background-quaternary);
|
||||
--button-border-color: var(--color-background-quaternary);
|
||||
--button-fg-color: var(--color-accent-primary);
|
||||
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-color: var(--color-background-quaternary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
inline-size: 100%;
|
||||
padding-block-end: 0;
|
||||
display: grid;
|
||||
gap: var(--sp-m);
|
||||
}
|
||||
|
||||
.title-wrapper {
|
||||
inline-size: 100%;
|
||||
padding-block-end: 0;
|
||||
display: grid;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.title {
|
||||
@include use-typography("title-large");
|
||||
|
||||
line-height: 1.2;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@include use-typography("title-small");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.tagline {
|
||||
@include use-typography("title-small");
|
||||
|
||||
margin: 0;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.links {
|
||||
@ -120,104 +132,10 @@
|
||||
gap: var(--sp-xxl);
|
||||
}
|
||||
|
||||
.register,
|
||||
.account,
|
||||
.recovery-request,
|
||||
.demo-account {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--sp-s);
|
||||
padding: 0;
|
||||
}
|
||||
.notification-email {
|
||||
@include use-typography("title-medium");
|
||||
|
||||
.register-text,
|
||||
.account-text,
|
||||
.recovery-text,
|
||||
.demo-account-text {
|
||||
@include use-typography("title-small");
|
||||
|
||||
text-align: right;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.register-link,
|
||||
.account-link,
|
||||
.recovery-link,
|
||||
.forgot-pass-link,
|
||||
.demo-account-link {
|
||||
@include use-typography("title-small");
|
||||
|
||||
text-align: left;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
display: inline;
|
||||
line-height: 1.2;
|
||||
color: var(--color-accent-primary);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.submit-btn,
|
||||
.register-btn,
|
||||
.recover-btn {
|
||||
@include use-typography("headline-small");
|
||||
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--color-accent-primary);
|
||||
border: $b-1 solid var(--color-accent-primary);
|
||||
color: var(--color-background-secondary);
|
||||
border-radius: $br-8;
|
||||
min-block-size: $sz-32;
|
||||
block-size: $sz-40;
|
||||
inline-size: 100%;
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--color-background-quaternary);
|
||||
border: $b-1 solid var(--color-background-quaternary);
|
||||
color: var(--color-foreground-disabled);
|
||||
cursor: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
@include use-typography("title-small");
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: px2rem(6);
|
||||
inline-size: 100%;
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-tertiary);
|
||||
color: var(--color-foreground-primary);
|
||||
|
||||
span {
|
||||
padding-block-start: var(--sp-xxs);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--color-foreground-primary);
|
||||
background-color: var(--color-background-quaternary);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--color-background-quaternary);
|
||||
border: 1px solid var(--color-background-quaternary);
|
||||
color: var(--color-foreground-disabled);
|
||||
cursor: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-buttons {
|
||||
display: flex;
|
||||
gap: var(--sp-s);
|
||||
margin-inline: $sz-36;
|
||||
}
|
||||
|
||||
@ -71,10 +71,17 @@
|
||||
(mf/defc login-form*
|
||||
[{:keys [params handle-redirect on-success-callback on-recovery-request origin] :as props}]
|
||||
(let [initial (mf/with-memo [params] params)
|
||||
error (mf/use-state false)
|
||||
|
||||
form (fm/use-form :schema schema:login-form
|
||||
:initial initial)
|
||||
|
||||
error (mf/use-state false)
|
||||
|
||||
show-password-field* (mf/use-state #(not (contains? cf/flags :login-with-custom-sso)))
|
||||
show-password-field? (deref show-password-field*)
|
||||
|
||||
callback-url (:callback-url params)
|
||||
|
||||
on-error
|
||||
(fn [cause]
|
||||
(let [cause (ex-data cause)]
|
||||
@ -102,12 +109,6 @@
|
||||
:else
|
||||
(reset! error (tr "errors.generic")))))
|
||||
|
||||
show-password-field*
|
||||
(mf/use-state #(not (contains? cf/flags :login-with-custom-sso)))
|
||||
|
||||
show-password-field?
|
||||
(deref show-password-field*)
|
||||
|
||||
on-success
|
||||
(fn [data]
|
||||
(when (fn? on-success-callback)
|
||||
@ -130,7 +131,8 @@
|
||||
(->> (rp/cmd! :get-sso-provider {:email (:email params)})
|
||||
(rx/map :id)
|
||||
(rx/catch (fn [cause]
|
||||
(log/error :hint "error on retrieving sso provider" :cause cause)
|
||||
(log/error :hint "error on retrieving sso provider"
|
||||
:cause cause)
|
||||
(rx/of nil)))
|
||||
(rx/subs! (fn [sso-provider-id]
|
||||
(if sso-provider-id
|
||||
@ -139,7 +141,7 @@
|
||||
(reset! show-password-field* true))))))))))
|
||||
|
||||
on-submit-ldap
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps form)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
@ -168,91 +170,102 @@
|
||||
{:level :error} message])
|
||||
|
||||
[:& fm/form {:on-submit on-submit
|
||||
:class (stl/css :login-form)
|
||||
:class (stl/css :form)
|
||||
:form form}
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/input
|
||||
{:name :email
|
||||
:type "email"
|
||||
:label (tr "auth.work-email")
|
||||
:class (stl/css :form-field)}]]
|
||||
[:div {:class (stl/css :form-row)}
|
||||
[:& fm/input {:name :email
|
||||
:type "email"
|
||||
:label (tr "auth.work-email")
|
||||
:class (stl/css :form-field)}]]
|
||||
|
||||
(when show-password-field?
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/input
|
||||
{:type "password"
|
||||
:name :password
|
||||
:auto-focus? true
|
||||
:label (tr "auth.password")
|
||||
:class (stl/css :form-field)}]])
|
||||
[:div {:class (stl/css :form-row)}
|
||||
[:& fm/input {:type "password"
|
||||
:name :password
|
||||
:auto-focus? true
|
||||
:label (tr "auth.password")
|
||||
:class (stl/css :form-field)}]])
|
||||
|
||||
(when (and (not= origin :viewer)
|
||||
(or (contains? cf/flags :login)
|
||||
(contains? cf/flags :login-with-password)))
|
||||
[:div {:class (stl/css :fields-row :forgot-password)}
|
||||
[:div {:class (stl/css :form-row :forgot-password-row)}
|
||||
[:> lk/link* {:action on-recovery-request
|
||||
:class (stl/css :forgot-pass-link)
|
||||
:class (stl/css :forgot-password-link)
|
||||
:data-testid "forgot-password"}
|
||||
(tr "auth.forgot-password")]])
|
||||
|
||||
[:div {:class (stl/css :buttons-stack)}
|
||||
[:div {:class (stl/css :form-submit-buttons)}
|
||||
(when (or (contains? cf/flags :login)
|
||||
(contains? cf/flags :login-with-password))
|
||||
[:> fm/submit-button*
|
||||
{:label (tr "labels.continue")
|
||||
:data-testid "login-submit"
|
||||
:class (stl/css :login-button)}])
|
||||
[:> fm/submit-button* {:label (tr "labels.continue")
|
||||
:data-testid "login-submit"
|
||||
:class (stl/css :form-submit-btn)}])
|
||||
|
||||
(when (contains? cf/flags :login-with-ldap)
|
||||
[:> fm/submit-button*
|
||||
{:label (tr "auth.login-with-ldap-submit")
|
||||
:class (stl/css :login-ldap-button)
|
||||
:on-click on-submit-ldap}])]]]))
|
||||
[:> fm/submit-button* {:label (tr "auth.login-with-ldap-submit")
|
||||
:class (stl/css :form-submit-btn)
|
||||
:on-click on-submit-ldap}])]]]))
|
||||
|
||||
(defn raw-icon
|
||||
[id]
|
||||
(mf/html
|
||||
[:> raw-svg* {:id id :class (stl/css :sso-icon)}]))
|
||||
|
||||
(mf/defc login-sso-buttons*
|
||||
(mf/defc sso-buttons*
|
||||
[{:keys [params] :as props}]
|
||||
(let [login-with-google (mf/use-fn (mf/deps params) #(login-with-sso "google" params))
|
||||
login-with-github (mf/use-fn (mf/deps params) #(login-with-sso "github" params))
|
||||
login-with-gitlab (mf/use-fn (mf/deps params) #(login-with-sso "gitlab" params))
|
||||
login-with-oidc (mf/use-fn (mf/deps params) #(login-with-sso "oidc" params))]
|
||||
(let [login-with-google
|
||||
(mf/use-fn
|
||||
(mf/deps params)
|
||||
#(login-with-sso "google" params))
|
||||
|
||||
[:div {:class (stl/css :auth-buttons)}
|
||||
login-with-github
|
||||
(mf/use-fn
|
||||
(mf/deps params)
|
||||
#(login-with-sso "github" params))
|
||||
|
||||
login-with-gitlab
|
||||
(mf/use-fn
|
||||
(mf/deps params)
|
||||
#(login-with-sso "gitlab" params))
|
||||
|
||||
login-with-oidc
|
||||
(mf/use-fn
|
||||
(mf/deps params)
|
||||
#(login-with-sso "oidc" params))]
|
||||
|
||||
[:div {:class (stl/css :sso-row)}
|
||||
(when (contains? cf/flags :login-with-google)
|
||||
[:> bl/button-link* {:on-click login-with-google
|
||||
:icon (raw-icon raw-icons/brand-google)
|
||||
:label (tr "auth.login-with-google-submit")
|
||||
:class (stl/css :login-btn :btn-google-auth)}])
|
||||
:class (stl/css :sso-btn)}])
|
||||
|
||||
(when (contains? cf/flags :login-with-github)
|
||||
[:> bl/button-link* {:on-click login-with-github
|
||||
:icon (raw-icon raw-icons/brand-github)
|
||||
:label (tr "auth.login-with-github-submit")
|
||||
:class (stl/css :login-btn :btn-github-auth)}])
|
||||
:class (stl/css :sso-btn)}])
|
||||
|
||||
(when (contains? cf/flags :login-with-gitlab)
|
||||
[:> bl/button-link* {:on-click login-with-gitlab
|
||||
:icon (raw-icon raw-icons/brand-gitlab)
|
||||
:label (tr "auth.login-with-gitlab-submit")
|
||||
:class (stl/css :login-btn :btn-gitlab-auth)}])
|
||||
:class (stl/css :sso-btn)}])
|
||||
|
||||
(when (contains? cf/flags :login-with-oidc)
|
||||
[:> bl/button-link* {:on-click login-with-oidc
|
||||
:icon (raw-icon raw-icons/brand-openid)
|
||||
:label (or (not-empty cf/oidc-name) (tr "auth.login-with-oidc-submit"))
|
||||
:class (stl/css :login-btn :btn-oidc-auth)}])]))
|
||||
:class (stl/css :sso-btn)}])]))
|
||||
|
||||
(mf/defc login-dialog*
|
||||
[{:keys [params] :as props}]
|
||||
[:*
|
||||
(when show-sso-login-buttons?
|
||||
[:*
|
||||
[:> login-sso-buttons* {:params params}]
|
||||
[:> sso-buttons* {:params params}]
|
||||
|
||||
(when (or (contains? cf/flags :login)
|
||||
(contains? cf/flags :login-with-password)
|
||||
@ -270,11 +283,11 @@
|
||||
(mf/use-fn
|
||||
#(st/emit! (rt/nav :auth-register params)))]
|
||||
|
||||
[:div {:class (stl/css :auth-form-wrapper)}
|
||||
[:h1 {:class (stl/css :auth-title)
|
||||
[:div {:class (stl/css :wrapper)}
|
||||
[:h1 {:class (stl/css :title)
|
||||
:data-testid "login-title"} (tr "auth.login-account-title")]
|
||||
|
||||
[:p {:class (stl/css :auth-tagline)}
|
||||
[:p {:class (stl/css :tagline)}
|
||||
(tr "auth.login-tagline")]
|
||||
|
||||
(when (contains? cf/flags :demo-warning)
|
||||
@ -286,7 +299,7 @@
|
||||
|
||||
[:div {:class (stl/css :links)}
|
||||
(when (contains? cf/flags :registration)
|
||||
[:div {:class (stl/css :register)}
|
||||
[:div {:class (stl/css :register-row)}
|
||||
[:span {:class (stl/css :register-text)}
|
||||
(tr "auth.register") " "]
|
||||
[:> lk/link* {:action go-register
|
||||
|
||||
@ -6,8 +6,88 @@
|
||||
|
||||
@use "./common";
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
|
||||
.forgot-password-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.forgot-password-link {
|
||||
@include use-typography("title-small");
|
||||
|
||||
text-align: left;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
display: inline;
|
||||
color: var(--color-accent-primary);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.form-submit-buttons {
|
||||
display: grid;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.sso-icon {
|
||||
max-inline-size: px2rem(26);
|
||||
max-block-size: px2rem(26);
|
||||
}
|
||||
|
||||
.sso-row {
|
||||
display: flex;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.sso-btn {
|
||||
--button-bg-color: var(--color-background-tertiary);
|
||||
--button-fg-color: var(--color-foreground-primary);
|
||||
|
||||
@include use-typography("title-small");
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: px2rem(6);
|
||||
inline-size: 100%;
|
||||
border-radius: $br-8;
|
||||
background-color: var(--button-bg-color);
|
||||
color: var(--button-fg-color);
|
||||
|
||||
&:hover {
|
||||
--button-bg-color: var(--color-background-quaternary);
|
||||
--button-fg-color: var(--color-foreground-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.register-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--sp-s);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.register-text {
|
||||
@include use-typography("title-small");
|
||||
|
||||
text-align: right;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.register-link {
|
||||
@include use-typography("title-small");
|
||||
|
||||
text-align: left;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
display: inline;
|
||||
color: var(--color-accent-primary);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,39 +60,38 @@
|
||||
:initial params)]
|
||||
|
||||
[:& fm/form {:on-submit on-submit
|
||||
:class (stl/css :recovery-form)
|
||||
:class (stl/css :form)
|
||||
:form form}
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:div {:class (stl/css :form-row)}
|
||||
[:& fm/input {:type "password"
|
||||
:name :password-1
|
||||
:show-success? true
|
||||
:label (tr "auth.new-password")
|
||||
:class (stl/css :form-field)}]]
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:div {:class (stl/css :form-row)}
|
||||
[:& fm/input {:type "password"
|
||||
:name :password-2
|
||||
:show-success? true
|
||||
:label (tr "auth.confirm-password")
|
||||
:class (stl/css :form-field)}]]
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:label (tr "auth.recovery-submit")
|
||||
:class (stl/css :submit-btn)}]]))
|
||||
[:> fm/submit-button* {:label (tr "auth.recovery-submit")
|
||||
:class (stl/css :form-submit-btn)}]]))
|
||||
|
||||
;; --- Recovery Request Page
|
||||
|
||||
(mf/defc recovery-page*
|
||||
[{:keys [params]}]
|
||||
[:div {:class (stl/css :auth-form-wrapper)}
|
||||
[:h1 {:class (stl/css :auth-title)} "Forgot your password?"]
|
||||
[:div {:class (stl/css :auth-subtitle)} "Please enter your new password"]
|
||||
[:div {:class (stl/css :wrapper)}
|
||||
[:h1 {:class (stl/css :title)} (tr "auth.recovery-request-title")]
|
||||
[:div {:class (stl/css :subtitle)} (tr "auth.recovery-request-subtitle")]
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
[:> recovery-form* {:params params}]
|
||||
|
||||
[:div {:class (stl/css :links)}
|
||||
[:div {:class (stl/css :go-back)}
|
||||
[:div {:class (stl/css :go-back-row)}
|
||||
[:a {:on-click #(st/emit! (rt/nav :auth-login))
|
||||
:class (stl/css :go-back-link)}
|
||||
(tr "profile.recovery.go-to-login")]]]])
|
||||
|
||||
@ -5,7 +5,3 @@
|
||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
@use "./common";
|
||||
|
||||
.submit-btn {
|
||||
margin-block-start: var(--sp-l);
|
||||
}
|
||||
|
||||
@ -70,18 +70,17 @@
|
||||
(st/emit! (du/request-profile-recovery params)))))]
|
||||
|
||||
[:& fm/form {:on-submit on-submit
|
||||
:class (stl/css :recovery-request-form)
|
||||
:class (stl/css :form)
|
||||
:form form}
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:div {:class (stl/css :form-row)}
|
||||
[:& fm/input {:name :email
|
||||
:label (tr "auth.work-email")
|
||||
:type "text"
|
||||
:class (stl/css :form-field)}]]
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:label (tr "auth.recovery-request-submit")
|
||||
:data-testid "recovery-resquest-submit"
|
||||
:class (stl/css :recover-btn)}]]))
|
||||
[:> fm/submit-button* {:label (tr "auth.recovery-request-submit")
|
||||
:data-testid "recovery-resquest-submit"
|
||||
:class (stl/css :form-submit-btn)}]]))
|
||||
|
||||
|
||||
;; --- Recovery Request Page
|
||||
@ -90,14 +89,14 @@
|
||||
[{:keys [params on-success-callback go-back-callback]}]
|
||||
(let [default-go-back #(st/emit! (rt/nav :auth-login))
|
||||
go-back (or go-back-callback default-go-back)]
|
||||
[:div {:class (stl/css :auth-form-wrapper)}
|
||||
[:h1 {:class (stl/css :auth-title)} (tr "auth.recovery-request-title")]
|
||||
[:div {:class (stl/css :auth-subtitle)} (tr "auth.recovery-request-subtitle")]
|
||||
[:div {:class (stl/css :wrapper)}
|
||||
[:h1 {:class (stl/css :title)} (tr "auth.recovery-request-title")]
|
||||
[:div {:class (stl/css :subtitle)} (tr "auth.recovery-request-subtitle")]
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
|
||||
[:> recovery-form* {:params params :on-success-callback on-success-callback}]
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
[:div {:class (stl/css :go-back)}
|
||||
[:div {:class (stl/css :go-back-row)}
|
||||
[:> lk/link* {:action go-back
|
||||
:class (stl/css :go-back-link)
|
||||
:data-testid "go-back-link"}
|
||||
@ -106,11 +105,10 @@
|
||||
|
||||
(mf/defc recovery-sent-page*
|
||||
[{:keys [email]}]
|
||||
[:div {:class (stl/css :auth-form-wrapper :register-success)}
|
||||
[:div {:class (stl/css :auth-title-wrapper)}
|
||||
[:h2 {:class (stl/css :auth-title)}
|
||||
[:div {:class (stl/css :wrapper :register-success)}
|
||||
[:div {:class (stl/css :title-wrapper)}
|
||||
[:h2 {:class (stl/css :title)}
|
||||
(tr "auth.check-email")]
|
||||
[:div {:class (stl/css :notification-text)} (tr "not-found.login.sent-recovery")]]
|
||||
[:div {:class (stl/css :notification-text-email)} email]
|
||||
[:div {:class (stl/css :notification-email)} email]
|
||||
[:div {:class (stl/css :notification-text)} (tr "not-found.login.sent-recovery-check")]])
|
||||
|
||||
|
||||
@ -4,19 +4,4 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
@use "./common";
|
||||
|
||||
.fields-row {
|
||||
margin-block-end: var(--sp-s);
|
||||
}
|
||||
|
||||
.notification-text-email {
|
||||
@include use-typography("title-medium");
|
||||
|
||||
line-height: 1.2;
|
||||
color: var(--color-accent-primary);
|
||||
margin-inline: $sz-36;
|
||||
}
|
||||
|
||||
@ -26,39 +26,38 @@
|
||||
|
||||
;; --- PAGE: Register
|
||||
|
||||
(mf/defc newsletter-options*
|
||||
(mf/defc newsletter-checkbox*
|
||||
{::mf/private true}
|
||||
[]
|
||||
(let [updates-label
|
||||
(let [newsletter-label
|
||||
(mf/html
|
||||
[:> i18n/tr-html*
|
||||
{:tag-name "div"
|
||||
:content (tr "onboarding-v2.newsletter.updates")}])]
|
||||
[:div {:class (stl/css :fields-row :input-visible :newsletter-option-wrapper)}
|
||||
[:> i18n/tr-html* {:tag-name "div"
|
||||
:content (tr "onboarding-v2.newsletter.updates")}])]
|
||||
|
||||
[:div {:class (stl/css :form-row :input-visible :newsletter-option-wrapper)}
|
||||
[:& fm/input {:name :accept-newsletter-updates
|
||||
:class (stl/css :checkbox-newsletter-updates)
|
||||
:type "checkbox"
|
||||
:default-checked false
|
||||
:label updates-label}]]))
|
||||
:label newsletter-label}]]))
|
||||
|
||||
(mf/defc terms-and-privacy*
|
||||
(mf/defc terms-and-privacy-checkbox*
|
||||
{::mf/private true}
|
||||
[]
|
||||
(let [terms-label
|
||||
(let [terms-and-privacy-label
|
||||
(mf/html
|
||||
[:> i18n/tr-html*
|
||||
{:tag-name "div"
|
||||
:content (tr "auth.terms-and-privacy-agreement"
|
||||
cf/terms-of-service-uri
|
||||
cf/privacy-policy-uri)}])]
|
||||
[:> i18n/tr-html* {:tag-name "div"
|
||||
:content (tr "auth.terms-and-privacy-agreement"
|
||||
cf/terms-of-service-uri
|
||||
cf/privacy-policy-uri)}])]
|
||||
|
||||
[:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)}
|
||||
[:div {:class (stl/css :form-row :input-visible :accept-terms-and-privacy-wrapper)}
|
||||
[:& fm/input {:name :accept-terms-and-privacy
|
||||
:show-error false
|
||||
:class (stl/css :checkbox-terms-and-privacy)
|
||||
:type "checkbox"
|
||||
:default-checked false
|
||||
:label terms-label}]]))
|
||||
:label terms-and-privacy-label}]]))
|
||||
|
||||
(def ^:private schema:register-form
|
||||
[:map {:title "RegisterForm"}
|
||||
@ -76,8 +75,7 @@
|
||||
form (fm/use-form :schema schema:register-form
|
||||
:initial initial)
|
||||
|
||||
submitted?
|
||||
(mf/use-state false)
|
||||
submitted? (mf/use-state false)
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
@ -167,22 +165,24 @@
|
||||
(->> (rp/cmd! :prepare-register-profile cdata)
|
||||
(rx/subs! on-register-profile on-error #(reset! submitted? false))))))]
|
||||
|
||||
[:& fm/form {:on-submit on-submit :form form}
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/form {:on-submit on-submit
|
||||
:form form
|
||||
:class (stl/css :form)}
|
||||
[:div {:class (stl/css :form-row)}
|
||||
|
||||
[:& fm/input {:name :fullname
|
||||
:label (tr "auth.fullname")
|
||||
:type "text"
|
||||
:show-success? true
|
||||
:class (stl/css :form-field)}]]
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:div {:class (stl/css :form-row)}
|
||||
[:& fm/input {:type "text"
|
||||
:name :email
|
||||
:label (tr "auth.work-email")
|
||||
:data-testid "email-input"
|
||||
:show-success? true
|
||||
:class (stl/css :form-field)}]]
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:div {:class (stl/css :form-row)}
|
||||
[:& fm/input {:name :password
|
||||
:hint (tr "auth.password-length-hint")
|
||||
:label (tr "auth.password")
|
||||
@ -191,21 +191,20 @@
|
||||
:class (stl/css :form-field)}]]
|
||||
|
||||
(when (contains? cf/flags :terms-and-privacy-checkbox)
|
||||
[:> terms-and-privacy*])
|
||||
[:> terms-and-privacy-checkbox*])
|
||||
|
||||
[:> newsletter-options*]
|
||||
[:> newsletter-checkbox*]
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:label (tr "auth.register-submit")
|
||||
:disabled @submitted?
|
||||
:data-testid "register-form-submit"
|
||||
:class (stl/css :register-btn)}]]))
|
||||
[:> fm/submit-button* {:label (tr "auth.register-submit")
|
||||
:disabled @submitted?
|
||||
:data-testid "register-form-submit"
|
||||
:class (stl/css :form-submit-btn)}]]))
|
||||
|
||||
(mf/defc register-methods*
|
||||
[{:keys [params hide-separator on-success-callback]}]
|
||||
[:*
|
||||
(when login/show-sso-login-buttons?
|
||||
[:> login/login-sso-buttons* {:params params}])
|
||||
[:> login/sso-buttons* {:params params}])
|
||||
(when (or login/show-sso-login-buttons? (false? hide-separator))
|
||||
[:hr {:class (stl/css :separator)}])
|
||||
(when (contains? cf/flags :login-with-password)
|
||||
@ -213,8 +212,8 @@
|
||||
|
||||
(mf/defc register-page*
|
||||
[{:keys [params]}]
|
||||
[:div {:class (stl/css :auth-form-wrapper :register-form)}
|
||||
[:h1 {:class (stl/css :auth-title)
|
||||
[:div {:class (stl/css :wrapper :register-form)}
|
||||
[:h1 {:class (stl/css :title)
|
||||
:data-testid "registration-title"} (tr "auth.register-title")]
|
||||
|
||||
(when (contains? cf/flags :demo-warning)
|
||||
@ -223,7 +222,7 @@
|
||||
[:> register-methods* {:params params}]
|
||||
|
||||
[:div {:class (stl/css :links)}
|
||||
[:div {:class (stl/css :account)}
|
||||
[:div {:class (stl/css :account-row)}
|
||||
[:span {:class (stl/css :account-text)} (tr "auth.already-have-account") " "]
|
||||
[:> lk/link* {:action #(st/emit! (rt/nav :auth-login params))
|
||||
:class (stl/css :account-link)
|
||||
@ -233,9 +232,9 @@
|
||||
(when (contains? cf/flags :demo-users)
|
||||
[:*
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
[:div {:class (stl/css :demo-account)}
|
||||
[:div {:class (stl/css :account-row)}
|
||||
[:> lk/link* {:action login/create-demo-profile
|
||||
:class (stl/css :demo-account-link)}
|
||||
:class (stl/css :account-link)}
|
||||
(tr "auth.create-demo-account")]]])]])
|
||||
|
||||
|
||||
@ -244,31 +243,31 @@
|
||||
(mf/defc register-success-page*
|
||||
[{:keys [params]}]
|
||||
(let [email (or (:email params) (::email storage/user))]
|
||||
[:div {:class (stl/css :auth-form-wrapper :register-success)}
|
||||
[:div {:class (stl/css :auth-title-wrapper)}
|
||||
[:h2 {:class (stl/css :auth-title)}
|
||||
[:div {:class (stl/css :wrapper :register-success)}
|
||||
[:div {:class (stl/css :title-wrapper)}
|
||||
[:h2 {:class (stl/css :register-success-title)}
|
||||
(tr "auth.check-email")]
|
||||
[:div {:class (stl/css :notification-text)} (tr "auth.verification-sent-email")]]
|
||||
[:div {:class (stl/css :notification-text-email)} email]]))
|
||||
[:div {:class (stl/css :notification-email)} email]]))
|
||||
|
||||
(mf/defc terms-register*
|
||||
(mf/defc terms-service-privacy-policy*
|
||||
[]
|
||||
(let [show-all? (and cf/terms-of-service-uri cf/privacy-policy-uri)
|
||||
show-terms? (some? cf/terms-of-service-uri)
|
||||
show-privacy? (some? cf/privacy-policy-uri)]
|
||||
|
||||
(when show-all?
|
||||
[:div {:class (stl/css :terms-register)}
|
||||
[:div {:class (stl/css :terms)}
|
||||
(when show-terms?
|
||||
[:a {:href cf/terms-of-service-uri :target "_blank" :class (stl/css :auth-link)}
|
||||
[:a {:href cf/terms-of-service-uri :target "_blank" :class (stl/css :terms-link)}
|
||||
(tr "auth.terms-of-service")])
|
||||
|
||||
(when show-all?
|
||||
[:span {:class (stl/css :and-text)}
|
||||
[:span {:class (stl/css :terms-and)}
|
||||
(dm/str " " (tr "labels.and") " ")])
|
||||
|
||||
(when show-privacy?
|
||||
[:a {:href cf/privacy-policy-uri :target "_blank" :class (stl/css :auth-link)}
|
||||
[:a {:href cf/privacy-policy-uri :target "_blank" :class (stl/css :terms-link)}
|
||||
(tr "auth.privacy-policy")])])))
|
||||
|
||||
;; --- PAGE: register validation
|
||||
@ -334,9 +333,9 @@
|
||||
|
||||
[:& fm/form {:on-submit on-submit
|
||||
:form form
|
||||
:class (stl/css :register-validate-form)}
|
||||
:class (stl/css :auth-form)}
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:div {:class (stl/css :form-row)}
|
||||
[:& fm/input {:name :fullname
|
||||
:label (tr "auth.fullname")
|
||||
:type "text"
|
||||
@ -344,30 +343,28 @@
|
||||
:class (stl/css :form-field)}]]
|
||||
|
||||
(when (contains? cf/flags :terms-and-privacy-checkbox)
|
||||
[:> terms-and-privacy*])
|
||||
[:> terms-and-privacy-checkbox*])
|
||||
|
||||
[:> newsletter-options*]
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:label (tr "auth.register-submit")
|
||||
:disabled @submitted?
|
||||
:class (stl/css :register-btn)}]]))
|
||||
[:> newsletter-checkbox*]
|
||||
|
||||
[:> fm/submit-button* {:label (tr "auth.register-submit")
|
||||
:disabled @submitted?
|
||||
:class (stl/css :form-submit-btn)}]]))
|
||||
|
||||
(mf/defc register-validate-page*
|
||||
[{:keys [params]}]
|
||||
[:div {:class (stl/css :auth-form-wrapper :register-form)}
|
||||
[:div {:class (stl/css :wrapper :register-form)}
|
||||
|
||||
|
||||
[:div {:class (stl/css :auth-title-wrapper)}
|
||||
[:h2 {:class (stl/css :auth-title)
|
||||
[:div {:class (stl/css :title-wrapper)}
|
||||
[:h2 {:class (stl/css :title)
|
||||
:data-testid "register-title"} (tr "auth.register-account-title")]
|
||||
[:div {:class (stl/css :auth-subtitle)} (tr "auth.register-account-tagline")]]
|
||||
[:div {:class (stl/css :subtitle)} (tr "auth.register-account-tagline")]]
|
||||
|
||||
[:> register-validate-form* {:params params}]
|
||||
|
||||
[:div {:class (stl/css :links)}
|
||||
[:div {:class (stl/css :go-back)}
|
||||
[:div {:class (stl/css :go-back-row)}
|
||||
[:> lk/link* {:action #(st/emit! (rt/nav :auth-register {}))
|
||||
:class (stl/css :go-back-link)}
|
||||
(tr "labels.go-back")]]]])
|
||||
|
||||
@ -4,57 +4,64 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
@use "./common";
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
@use "./common";
|
||||
|
||||
.checkbox-terms-and-privacy,
|
||||
.checkbox-newsletter-updates {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.checkbox-terms-and-privacy {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.register-form {
|
||||
gap: var(--sp-xxl);
|
||||
}
|
||||
|
||||
.account-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--sp-s);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.account-text {
|
||||
@include use-typography("title-small");
|
||||
|
||||
text-align: right;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.account-link {
|
||||
@include use-typography("title-small");
|
||||
|
||||
text-align: left;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
display: inline;
|
||||
color: var(--color-accent-primary);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.register-success {
|
||||
gap: var(--sp-xxl);
|
||||
}
|
||||
|
||||
.register-success .auth-title {
|
||||
.register-success-title {
|
||||
@include use-typography("title-medium");
|
||||
|
||||
line-height: 1.2;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.notification-text {
|
||||
@include use-typography("body-medium");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.notification-text-email {
|
||||
@include use-typography("title-medium");
|
||||
|
||||
line-height: 1.2;
|
||||
color: var(--color-accent-primary);
|
||||
margin-inline: $sz-36;
|
||||
}
|
||||
|
||||
.logo-btn {
|
||||
block-size: $sz-40;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
inline-size: $sz-120;
|
||||
margin-block-end: var(--sp-xxl);
|
||||
}
|
||||
|
||||
.terms-register {
|
||||
.terms {
|
||||
@include use-typography("body-small");
|
||||
|
||||
display: flex;
|
||||
@ -63,15 +70,15 @@
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.and-text {
|
||||
border-block-end: $b-1 solid transparent;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
.terms-link {
|
||||
color: var(--color-accent-primary);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.terms-and {
|
||||
border-block-end: $b-1 solid transparent;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
@ -114,6 +114,7 @@
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-block-size: 0;
|
||||
outline: $b-1 solid var(--tab-panel-outline-color);
|
||||
}
|
||||
|
||||
|
||||
@ -185,7 +185,7 @@
|
||||
:on-click set-section} (tr "auth.login-here")]]
|
||||
[:div {:class (stl/css :links)}
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
[:> register/terms-register*]]]
|
||||
[:> register/terms-service-privacy-policy*]]]
|
||||
|
||||
:register-validate
|
||||
[:div {:class (stl/css :form-container)}
|
||||
|
||||
@ -13,8 +13,10 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.auth.login :refer [login-dialog*]]
|
||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page*]]
|
||||
[app.main.ui.auth.register :refer [register-methods* register-success-page* terms-register* register-validate-form*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.auth.register :refer [register-methods* register-success-page*
|
||||
register-validate-form* terms-service-privacy-policy*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
@ -25,14 +27,15 @@
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :login-register}
|
||||
[_]
|
||||
(let [user-email (mf/use-state "")
|
||||
(let [user-email (mf/use-state "")
|
||||
register-token (mf/use-state "")
|
||||
|
||||
current-section* (mf/use-state :login)
|
||||
current-section (deref current-section*)
|
||||
current-section (deref current-section*)
|
||||
|
||||
set-current-section
|
||||
(mf/use-fn #(reset! current-section* %))
|
||||
(mf/use-fn
|
||||
#(reset! current-section* %))
|
||||
|
||||
set-section
|
||||
(mf/use-fn
|
||||
@ -42,7 +45,9 @@
|
||||
(keyword))]
|
||||
(set-current-section section))))
|
||||
|
||||
go-back-to-login (mf/use-fn #(set-current-section :login))
|
||||
go-back-to-login
|
||||
(mf/use-fn
|
||||
#(set-current-section :login))
|
||||
|
||||
main-section (or
|
||||
(= current-section :login)
|
||||
@ -52,13 +57,16 @@
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (modal/hide)))
|
||||
|
||||
success-email-sent
|
||||
(fn [email]
|
||||
(reset! user-email email)
|
||||
(set-current-section :email-sent))
|
||||
|
||||
success-login
|
||||
(fn []
|
||||
(.reload js/window.location true))
|
||||
|
||||
success-register
|
||||
(fn [data]
|
||||
(reset! register-token (:token data))
|
||||
@ -67,49 +75,48 @@
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h2 {:class (stl/css :modal-title)} (tr "labels.continue-with-penpot")]
|
||||
[:button {:class (stl/css :modal-close-btn)
|
||||
:title (tr "labels.close")
|
||||
:on-click close} deprecated-icon/close]]
|
||||
[:h2 {:class (stl/css :modal-header-title)} (tr "labels.continue-with-penpot")]
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:class (stl/css :modal-close)
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click close
|
||||
:icon i/close}]]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
(case current-section
|
||||
:login
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:> login-dialog*
|
||||
{:on-success-callback success-login
|
||||
:origin :viewer}]
|
||||
[:div {:class (stl/css :links)}
|
||||
[:div {:class (stl/css :recovery-request)}
|
||||
[:div {:class (stl/css :login-form)}
|
||||
[:> login-dialog* {:on-success-callback success-login
|
||||
:origin :viewer}]
|
||||
[:div {:class (stl/css :login-links)}
|
||||
[:div
|
||||
[:a {:on-click set-section
|
||||
:class (stl/css :recovery-link)
|
||||
:data-value "recovery-request"}
|
||||
(tr "auth.forgot-password")]]
|
||||
(when (contains? cf/flags :registration)
|
||||
[:div {:class (stl/css :register)}
|
||||
[:span {:class (stl/css :register-text)}
|
||||
[:div
|
||||
[:span
|
||||
(tr "auth.register") " "]
|
||||
[:a {:on-click set-section
|
||||
:class (stl/css :register-link)
|
||||
:data-value "register"}
|
||||
(tr "auth.register-submit")]])]]
|
||||
|
||||
:register
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:div {:class (stl/css :login-form)}
|
||||
[:> register-methods* {:on-success-callback success-register}]
|
||||
[:div {:class (stl/css :links)}
|
||||
[:div {:class (stl/css :account)}
|
||||
[:div {:class (stl/css :login-links)}
|
||||
[:div
|
||||
[:span (tr "auth.already-have-account") " "]
|
||||
[:a {:on-click set-section
|
||||
:data-value "login"}
|
||||
(tr "auth.login-here")]]]]
|
||||
|
||||
:register-validate
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:div {:class (stl/css :login-form)}
|
||||
[:> register-validate-form* {:params {:token @register-token}
|
||||
:on-success-callback success-email-sent}]
|
||||
[:div {:class (stl/css :links)}
|
||||
[:div {:class (stl/css :register)}
|
||||
[:div {:class (stl/css :login-links)}
|
||||
[:div
|
||||
[:a {:on-click set-section
|
||||
:data-value "register"}
|
||||
(tr "labels.go-back")]]]]
|
||||
@ -117,10 +124,11 @@
|
||||
:recovery-request
|
||||
[:> recovery-request-page* {:go-back-callback go-back-to-login
|
||||
:on-success-callback success-email-sent}]
|
||||
|
||||
:email-sent
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:div {:class (stl/css :login-form)}
|
||||
[:> register-success-page* {:params {:email @user-email}}]])
|
||||
|
||||
(when main-section
|
||||
[:div {:class (stl/css :links)}
|
||||
[:> terms-register*]])]]]))
|
||||
[:div {:class (stl/css :login-links)}
|
||||
[:> terms-service-privacy-policy*]])]]]))
|
||||
|
||||
@ -4,75 +4,71 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
@use "ds/z-index.scss" as *;
|
||||
|
||||
.modal-overlay {
|
||||
@extend %modal-overlay-base;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
z-index: var(--z-index-set);
|
||||
background-color: var(--color-overlay-default);
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
@extend %modal-container-base;
|
||||
|
||||
width: deprecated.$s-368;
|
||||
position: relative;
|
||||
padding: var(--sp-xxxl);
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-primary);
|
||||
border: $b-2 solid var(--color-background-quaternary);
|
||||
min-inline-size: $sz-364;
|
||||
min-block-size: $sz-192;
|
||||
max-inline-size: $sz-512;
|
||||
max-block-size: $sz-712;
|
||||
inline-size: $sz-364;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: deprecated.$s-24;
|
||||
margin-block-end: var(--sp-xxl);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include deprecated.uppercase-title-typography;
|
||||
.modal-header-title {
|
||||
@include use-typography("headline-small");
|
||||
|
||||
color: var(--modal-title-foreground-color);
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
@extend %modal-close-btn-base;
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
inset-block-start: var(--sp-s);
|
||||
inset-inline-end: var(--sp-s);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@include deprecated.flex-column;
|
||||
@include deprecated.body-small-typography;
|
||||
@include use-typography("body-small");
|
||||
|
||||
gap: deprecated.$s-24;
|
||||
max-height: deprecated.$s-400;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xxl);
|
||||
max-block-size: px2rem(576);
|
||||
overflow: hidden auto;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
.login-form {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-m);
|
||||
}
|
||||
|
||||
.links {
|
||||
.login-links {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.link-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: deprecated.$s-12;
|
||||
|
||||
span {
|
||||
text-align: center;
|
||||
font-size: deprecated.$fs-14;
|
||||
color: var(--modal-text-foreground-color);
|
||||
margin-top: deprecated.$s-12;
|
||||
}
|
||||
|
||||
a {
|
||||
@extend %button-secondary;
|
||||
|
||||
height: deprecated.$s-40;
|
||||
text-transform: uppercase;
|
||||
font-size: deprecated.$fs-11;
|
||||
}
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
@ -97,9 +97,7 @@
|
||||
sitemap-height
|
||||
(if sitemap-collapsed? 32 height)]
|
||||
|
||||
[:article {:class (stl/css :layers-tab)
|
||||
:style {:--height (dm/str height "px")}}
|
||||
|
||||
[:article {:class (stl/css :layers-tab)}
|
||||
[:> sitemap* {:layout layout
|
||||
:height sitemap-height
|
||||
:collapsed sitemap-collapsed?
|
||||
|
||||
@ -79,13 +79,15 @@
|
||||
|
||||
.layers-tab {
|
||||
padding-block-start: var(--sp-xs);
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.layers-tab-resize-area {
|
||||
background-color: var(--color-background-primary);
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
flex: 0 0 auto;
|
||||
inline-size: 100%;
|
||||
padding: px2rem(3) 0 px2rem(1);
|
||||
block-size: $sz-6;
|
||||
@ -120,11 +122,16 @@
|
||||
.left-sidebar-content {
|
||||
grid-area: content;
|
||||
inset-inline-end: calc(-1 * var(--sp-s));
|
||||
min-block-size: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.left-sidebar-tabs {
|
||||
--tabs-nav-padding-inline-start: var(--sp-m);
|
||||
--tabs-nav-padding-inline-end: var(--sp-m);
|
||||
|
||||
block-size: 100%;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.left-sidebar-resize-area {
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-xs);
|
||||
margin: var(--sp-m) var(--sp-m) 0 var(--sp-m);
|
||||
margin: px2rem(6) var(--sp-m) 0 var(--sp-m);
|
||||
}
|
||||
|
||||
.tool-window-bar-title {
|
||||
@ -31,11 +31,10 @@
|
||||
}
|
||||
|
||||
.tool-window-content {
|
||||
--calculated-height: calc(#{px2rem(136)} + var(--height, #{$sz-200}));
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
block-size: calc(100vh - var(--calculated-height));
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
inline-size: calc(var(--left-sidebar-width) + var(--depth) * var(--layer-indentation-size));
|
||||
overflow: auto;
|
||||
scrollbar-gutter: stable;
|
||||
@ -117,6 +116,10 @@
|
||||
|
||||
.layers {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.replace-wrapper {
|
||||
|
||||
@ -15,7 +15,8 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
flex: 0 0 auto;
|
||||
flex-shrink: 0;
|
||||
inline-size: 100%;
|
||||
block-size: var(--height, $sz-200);
|
||||
}
|
||||
|
||||
@ -150,16 +150,26 @@
|
||||
|
||||
on-main-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps open)
|
||||
(fn [event]
|
||||
(cond
|
||||
(kbd/space? event)
|
||||
(and open (kbd/esc? event))
|
||||
(reset! open* false)
|
||||
|
||||
(or (kbd/enter? event) (kbd/space? event))
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(if open
|
||||
(reset! open* false)
|
||||
(do
|
||||
(cancel-timer! close-timer*)
|
||||
(reset! open* true))))
|
||||
|
||||
(kbd/down-arrow? event)
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(cancel-timer! close-timer*)
|
||||
(reset! open* true))
|
||||
|
||||
(and open (kbd/esc? event))
|
||||
(reset! open* false))))
|
||||
(reset! open* true)))))
|
||||
|
||||
on-flyout-key-down
|
||||
(mf/use-fn
|
||||
@ -219,7 +229,10 @@
|
||||
:aria-expanded open
|
||||
:has-tooltip false
|
||||
:icon default-icon
|
||||
:on-click on-select-tool
|
||||
:on-click (fn [event]
|
||||
(cancel-timer! open-timer*)
|
||||
(cancel-timer! close-timer*)
|
||||
(on-select-tool event))
|
||||
:on-key-down on-main-key-down
|
||||
:data-tool (name default-tool)}]
|
||||
|
||||
|
||||
@ -282,8 +282,7 @@
|
||||
[code value]
|
||||
(if (some? value)
|
||||
(throw (js/Error. (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code)))
|
||||
(throw (js/Error. (dm/str "[PENPOT PLUGIN] Value not valid. Code: " code))))
|
||||
nil)
|
||||
(throw (js/Error. (dm/str "[PENPOT PLUGIN] Value not valid. Code: " code)))))
|
||||
|
||||
(defn not-valid
|
||||
[plugin-id code value]
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
"@types/node": "^26.1.2",
|
||||
"@vitest/browser": "^4.1.10",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"@vitest/ui": "^4.1.10",
|
||||
"@vitest/ui": "^4.1.11",
|
||||
"canvas": "^3.2.3",
|
||||
"esbuild": "^0.28.0",
|
||||
"jsdom": "^30.0.1",
|
||||
@ -28,5 +28,5 @@
|
||||
"vite": "^8.2.0",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee"
|
||||
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67"
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"version": "1.2.0-RC1",
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC Sucursal en España SL",
|
||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
||||
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -36,7 +36,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.1.2",
|
||||
"@zip.js/zip.js": "2.8.34",
|
||||
"concurrently": "^10.0.4",
|
||||
"concurrently": "^10.0.5",
|
||||
"date-fns": "^4.4.0",
|
||||
"nodemon": "^3.1.14",
|
||||
"source-map-support": "^0.5.21"
|
||||
|
||||
111
library/pnpm-lock.yaml
generated
111
library/pnpm-lock.yaml
generated
@ -1,3 +1,104 @@
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
configDependencies: {}
|
||||
packageManagerDependencies:
|
||||
pnpm:
|
||||
specifier: 12.0.0
|
||||
version: 12.0.0
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.0.0':
|
||||
resolution: {integrity: sha512-sqeoPfVMIfQhbwzDrKraXY2ynyuWClFqzvfImzAS/yczEru1m5SGvQ9kgFPDvQzJZ9AetedgJeDZC6qYvH8/tQ==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.0.0':
|
||||
resolution: {integrity: sha512-Quc3J6c9cGTy+LDgz1cLVgCNOU9IERuyAlDoEj0DCilKqvo50Jx1GV8k74iwn4J9fFSKkm8JrwNvtTDj3uWnUA==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.0.0':
|
||||
resolution: {integrity: sha512-EVWd3OTmgsMFhXx69b5JxIzoabG9Ma7m4OeTaf0ZKBzMnfYi8u21NDQo92ToMrdYL5dYDDCHsyYIjXzk+d0HhA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.0.0':
|
||||
resolution: {integrity: sha512-cXHHW8M4rAPsYNkKZO9WVcpLLK55i9EaIsZPfIqUuY2eopd5LqnFyBge54HCh1GC0yCX8ySn0hYIi+4OyAEoDg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.0.0':
|
||||
resolution: {integrity: sha512-UcXwMdFjly0mpddkGigHKTxe27IMv2fUK4IWW/MHmJ3yMguxXmkwNlEI4aE+G1HO2TLo20uNEUWD4ymLe/DaCQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-x64@12.0.0':
|
||||
resolution: {integrity: sha512-6Rsl+zEWMOmus7v7/9J3OE8EMvHyNAfxYmDfmhQG4J0985OuT3G3Ho9NSGHjkBn4aU4bgklWifRhe1HX8dUSyw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.0.0':
|
||||
resolution: {integrity: sha512-O5F76A4oVFrpDGdFxEszRIThOSBfjHdH5c006gR+7UTCfiXrukr1XfqPungUI1DXcSR5gb9jBsPQqQZOAoOOxw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@pnpm/exe.win32-x64@12.0.0':
|
||||
resolution: {integrity: sha512-5dKFajIEWJ1ai+KHXFJvskY6vchbunmLwSUV2ywbLymcmJjfY5XJVpgzPCyIoVCMVG0zHorr66+hM8h8b3aRfQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
pnpm@12.0.0:
|
||||
resolution: {integrity: sha512-ni49w5EZlYaNyUuBdcIXwn6VQI+gO0oidJd48rNPdzt3zdOznt6BcbIvzVO+ajU0Lp+smUimjvWN9kiM6Jp+Zw==}
|
||||
engines: {node: '>=18.*'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.0.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-x64@12.0.0':
|
||||
optional: true
|
||||
|
||||
pnpm@12.0.0:
|
||||
optionalDependencies:
|
||||
'@pnpm/exe.darwin-arm64': 12.0.0
|
||||
'@pnpm/exe.darwin-x64': 12.0.0
|
||||
'@pnpm/exe.linux-arm64': 12.0.0
|
||||
'@pnpm/exe.linux-arm64-musl': 12.0.0
|
||||
'@pnpm/exe.linux-x64': 12.0.0
|
||||
'@pnpm/exe.linux-x64-musl': 12.0.0
|
||||
'@pnpm/exe.win32-arm64': 12.0.0
|
||||
'@pnpm/exe.win32-x64': 12.0.0
|
||||
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
@ -18,8 +119,8 @@ importers:
|
||||
specifier: 2.8.34
|
||||
version: 2.8.34(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95)
|
||||
concurrently:
|
||||
specifier: ^10.0.4
|
||||
version: 10.0.4
|
||||
specifier: ^10.0.5
|
||||
version: 10.0.5
|
||||
date-fns:
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0
|
||||
@ -82,8 +183,8 @@ packages:
|
||||
resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
concurrently@10.0.4:
|
||||
resolution: {integrity: sha512-trZql+7l/0+WRAsAnEdctr4+iiOS6ZrViI6H8QWcCF9MFS/LT0dKpe8vluB1to6it+OxSI4VospFTIFMW8DJRw==}
|
||||
concurrently@10.0.5:
|
||||
resolution: {integrity: sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==}
|
||||
engines: {node: '>=22'}
|
||||
hasBin: true
|
||||
|
||||
@ -304,7 +405,7 @@ snapshots:
|
||||
strip-ansi: 7.2.0
|
||||
wrap-ansi: 9.0.2
|
||||
|
||||
concurrently@10.0.4:
|
||||
concurrently@10.0.5:
|
||||
dependencies:
|
||||
chalk: 5.6.2
|
||||
rxjs: 7.8.2
|
||||
|
||||
61
manage.sh
61
manage.sh
@ -223,8 +223,10 @@ function ensure-devenv-network {
|
||||
# those stale values would leak into substitution. And because Docker Compose
|
||||
# gives shell-env precedence over --env-file, the re-injected per-instance
|
||||
# overrides cleanly override the defaults.env baseline. Re-injected: HOME/PATH
|
||||
# (tooling), CURRENT_USER_ID/PENPOT_SOURCE_PATH (always per-call), and the
|
||||
# instance-env-overrides block.
|
||||
# (tooling), CURRENT_USER_ID/PENPOT_SOURCE_PATH (always per-call), the
|
||||
# optional PENPOT_OPENCODE_CONFIG_DIR (set only by run-devenv's
|
||||
# --opencode-config-dir within this process), and the instance-env-overrides
|
||||
# block.
|
||||
function infra-compose {
|
||||
env -i HOME="$HOME" PATH="$PATH" PWD="$PWD" \
|
||||
docker compose -p penpotdev-infra \
|
||||
@ -245,14 +247,26 @@ function instance-compose {
|
||||
# Per-instance overrides apply to all workspaces uniformly.
|
||||
mapfile -t overrides < <(instance-env-overrides "$instance")
|
||||
|
||||
# Optional personal-opencode-config overlay: the extra -f and variable
|
||||
# are only present when run-devenv --opencode-config-dir resolved a host
|
||||
# directory into PENPOT_OPENCODE_CONFIG_DIR; when unset neither the file
|
||||
# nor the variable is referenced, so default behaviour is unchanged.
|
||||
local -a compose_files=(-f docker/devenv/docker-compose.main.yml)
|
||||
local -a opencode_env=()
|
||||
if [[ -n "${PENPOT_OPENCODE_CONFIG_DIR:-}" ]]; then
|
||||
compose_files+=(-f docker/devenv/docker-compose.opencode.yml)
|
||||
opencode_env=("PENPOT_OPENCODE_CONFIG_DIR=${PENPOT_OPENCODE_CONFIG_DIR}")
|
||||
fi
|
||||
|
||||
env -i HOME="$HOME" PATH="$PATH" PWD="$PWD" \
|
||||
CURRENT_USER_ID="${CURRENT_USER_ID:-$(id -u)}" \
|
||||
PENPOT_SOURCE_PATH="$source_path" \
|
||||
DEVENV_TAG="$DEVENV_TAG" \
|
||||
"${opencode_env[@]}" \
|
||||
"${overrides[@]}" \
|
||||
docker compose -p "penpotdev-${instance}" \
|
||||
--env-file "$DEVENV_DEFAULTS_FILE" \
|
||||
-f docker/devenv/docker-compose.main.yml \
|
||||
"${compose_files[@]}" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
@ -658,6 +672,26 @@ function parse-ws-integer {
|
||||
echo "ws$raw"
|
||||
}
|
||||
|
||||
# Strict parser for --opencode-config-dir. Resolves the value to an absolute
|
||||
# host directory (docker compose resolves relative bind-mount sources against
|
||||
# the compose file's directory, not $PWD, so relative values would be
|
||||
# misinterpreted) and verifies it exists. Echoes the absolute path; anything
|
||||
# else fails fast.
|
||||
function parse-opencode-config-dir {
|
||||
local raw="$1"
|
||||
if [[ -z "$raw" ]]; then
|
||||
echo "Invalid --opencode-config-dir: value is empty." >&2
|
||||
return 1
|
||||
fi
|
||||
raw="${raw/#\~/$HOME}"
|
||||
local abs
|
||||
if ! abs=$(realpath -e "$raw" 2>/dev/null) || [[ ! -d "$abs" ]]; then
|
||||
echo "Invalid --opencode-config-dir: '$raw' is not an existing directory." >&2
|
||||
return 1
|
||||
fi
|
||||
echo "$abs"
|
||||
}
|
||||
|
||||
|
||||
# Bring a single instance up: compose up + detached tmux start. When agentic
|
||||
# is true (the default) the tmux session gets MCP + Serena enabled; when false
|
||||
@ -764,6 +798,7 @@ function run-devenv {
|
||||
local serena_context="desktop-app"
|
||||
local git_user_name=""
|
||||
local git_user_email=""
|
||||
local opencode_config_dir=""
|
||||
local -a extra_env_args=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@ -778,6 +813,8 @@ function run-devenv {
|
||||
do_attach=true; shift;;
|
||||
--serena-context)
|
||||
serena_context="$2"; shift 2;;
|
||||
--opencode-config-dir)
|
||||
opencode_config_dir="$(parse-opencode-config-dir "$2")" || return 1; shift 2;;
|
||||
--git-user-name)
|
||||
git_user_name="$2"; shift 2;;
|
||||
--git-user-email)
|
||||
@ -787,13 +824,16 @@ function run-devenv {
|
||||
-e*)
|
||||
extra_env_args+=(-e "${1#-e}"); shift;;
|
||||
-h|--help)
|
||||
echo "Usage: run-devenv [--ws N] [--sync] [--attach] [--agentic] [--serena-context CTX] [--git-user-name NAME] [--git-user-email EMAIL] [-e KEY=VAL]"
|
||||
echo "Usage: run-devenv [--ws N] [--sync] [--attach] [--agentic] [--serena-context CTX] [--opencode-config-dir DIR] [--git-user-name NAME] [--git-user-email EMAIL] [-e KEY=VAL]"
|
||||
echo " Bring a single workspace up."
|
||||
echo " --ws N target workspace (default: 0)."
|
||||
echo " --sync re-seed the wsN clone from the live repo (forbidden on ws0)."
|
||||
echo " --attach attach to the tmux session after startup."
|
||||
echo " --agentic enable MCP + Serena (AI-agent mode)."
|
||||
echo " --serena-context CTX context passed to Serena (default: desktop-app)."
|
||||
echo " --opencode-config-dir DIR bind-mount DIR at ~/.config/opencode inside the"
|
||||
echo " container (personal agents/prompts/skills kept in a"
|
||||
echo " separate repo). Applied at container creation."
|
||||
echo " --git-user-name NAME git author name inside the container (default: host git config)."
|
||||
echo " --git-user-email EMAIL git author email inside the container."
|
||||
echo " -e KEY=VAL forward env var to docker exec on attach."
|
||||
@ -863,6 +903,14 @@ function run-devenv {
|
||||
write-instance-mcp-configs "$target"
|
||||
fi
|
||||
|
||||
# Scope the personal opencode config to this process only: instance-compose
|
||||
# includes the overlay compose file and the variable just when it is set,
|
||||
# so instances brought up without the flag mount nothing.
|
||||
if [[ -n "$opencode_config_dir" ]]; then
|
||||
echo "[$target] mounting personal opencode config: $opencode_config_dir -> ~/.config/opencode"
|
||||
export PENPOT_OPENCODE_CONFIG_DIR="$opencode_config_dir"
|
||||
fi
|
||||
|
||||
echo "Starting $target..."
|
||||
start-instance "$target" "$serena_context" "$git_user_name" "$git_user_email" "$agentic"
|
||||
print-instance-info "$target"
|
||||
@ -1338,6 +1386,11 @@ function usage {
|
||||
echo " --attach attach to the tmux session after startup."
|
||||
echo " --agentic enable MCP + Serena (AI-agent mode)."
|
||||
echo " --serena-context CTX passed to Serena (default: desktop-app)."
|
||||
echo " --opencode-config-dir DIR"
|
||||
echo " bind-mount DIR over the container's"
|
||||
echo " ~/.config/opencode (personal opencode"
|
||||
echo " agents/prompts/skills kept outside this"
|
||||
echo " repo; applied at container creation)."
|
||||
echo " -e KEY=VAL forwarded to 'docker exec' on attach."
|
||||
echo " --git-user-name NAME / --git-user-email EMAIL"
|
||||
echo " identity wired into the container's git config"
|
||||
|
||||
@ -265,6 +265,7 @@ The Penpot MCP server can be configured using environment variables.
|
||||
| `PENPOT_MCP_SERVER_PORT` | Port for the HTTP/SSE server | `4401` |
|
||||
| `PENPOT_MCP_WEBSOCKET_PORT` | Port for the WebSocket server (plugin connection) | `4402` |
|
||||
| `PENPOT_MCP_REPL_PORT` | Port for the REPL server (development/debugging) | `4403` |
|
||||
| `PENPOT_MCP_REPL_ENABLE` | Explicitly enable/disable the REPL server. Set to `true` to enable. When unset, defaults to the value of `PENPOT_MCP_DEVENV`. | (unset) |
|
||||
| `PENPOT_MCP_REMOTE_MODE` | Enable remote mode (disables file system access). Set to `true` to enable. | `false` |
|
||||
| `PENPOT_MCP_DEVENV` | Enable Penpot development environment tools. Set to `true` to enable. | `false` |
|
||||
| `PENPOT_MCP_TOOL_TIMEOUT_S` | Timeout, in seconds, for tool calls dispatched to the Penpot plugin | `120` |
|
||||
|
||||
@ -23,9 +23,9 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot.git"
|
||||
},
|
||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
||||
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||
"devDependencies": {
|
||||
"concurrently": "^10.0.4",
|
||||
"concurrently": "^10.0.5",
|
||||
"prettier": "^3.9.6"
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"description": "Shared type definitions and interfaces for Penpot MCP",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
||||
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||
"scripts": {
|
||||
"build": "tsc --build --clean && tsc --build",
|
||||
"watch": "tsc --watch",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { defineConfig, mergeConfig } from "vite";
|
||||
import baseConfig from "./vite.config";
|
||||
import baseConfig from "./vite.config.ts";
|
||||
|
||||
export default mergeConfig(
|
||||
baseConfig,
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
||||
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
|
||||
149
mcp/packages/server/src/PenpotMcpServer.test.ts
Normal file
149
mcp/packages/server/src/PenpotMcpServer.test.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { PenpotMcpServer } from "./PenpotMcpServer";
|
||||
|
||||
// ── Pure function tests ────────────────────────────────────────
|
||||
|
||||
test("isDevEnvEnabled returns false when PENPOT_MCP_DEVENV is not set", () => {
|
||||
assert.equal(PenpotMcpServer.isDevEnvEnabled({}), false);
|
||||
});
|
||||
|
||||
test("isDevEnvEnabled returns false when PENPOT_MCP_DEVENV is 'false'", () => {
|
||||
assert.equal(PenpotMcpServer.isDevEnvEnabled({ PENPOT_MCP_DEVENV: "false" }), false);
|
||||
});
|
||||
|
||||
test("isDevEnvEnabled returns true when PENPOT_MCP_DEVENV is 'true'", () => {
|
||||
assert.equal(PenpotMcpServer.isDevEnvEnabled({ PENPOT_MCP_DEVENV: "true" }), true);
|
||||
});
|
||||
|
||||
// ── Pure function tests: isReplEnabled ──────────────────────────
|
||||
|
||||
test("isReplEnabled returns false when neither env var is set", () => {
|
||||
assert.equal(PenpotMcpServer.isReplEnabled({}), false);
|
||||
});
|
||||
|
||||
test("isReplEnabled returns true when PENPOT_MCP_DEVENV is 'true' (fallback)", () => {
|
||||
assert.equal(PenpotMcpServer.isReplEnabled({ PENPOT_MCP_DEVENV: "true" }), true);
|
||||
});
|
||||
|
||||
test("isReplEnabled returns true when PENPOT_MCP_REPL_ENABLE is 'true'", () => {
|
||||
assert.equal(PenpotMcpServer.isReplEnabled({ PENPOT_MCP_REPL_ENABLE: "true" }), true);
|
||||
});
|
||||
|
||||
test("isReplEnabled returns false when PENPOT_MCP_REPL_ENABLE is 'false' even if DEVENV is true", () => {
|
||||
assert.equal(PenpotMcpServer.isReplEnabled({ PENPOT_MCP_REPL_ENABLE: "false", PENPOT_MCP_DEVENV: "true" }), false);
|
||||
});
|
||||
|
||||
test("isReplEnabled returns true when PENPOT_MCP_REPL_ENABLE is 'true' regardless of DEVENV", () => {
|
||||
assert.equal(PenpotMcpServer.isReplEnabled({ PENPOT_MCP_REPL_ENABLE: "true" }), true);
|
||||
});
|
||||
|
||||
// ── Integration tests: constructor gating ──────────────────────
|
||||
//
|
||||
// Each test uses unique ports to avoid conflicts when tests run
|
||||
// in the same process. The server is stopped in the finally block
|
||||
// to release the WebSocket port.
|
||||
|
||||
let portCounter = 14_500;
|
||||
function uniquePorts() {
|
||||
const base = portCounter;
|
||||
portCounter += 10;
|
||||
return { server: base, ws: base + 1, repl: base + 2 };
|
||||
}
|
||||
|
||||
test("constructor does not create ReplServer when PENPOT_MCP_DEVENV is unset", async () => {
|
||||
const prev = process.env.PENPOT_MCP_DEVENV;
|
||||
const prevPorts = setUniqueEnv();
|
||||
delete process.env.PENPOT_MCP_DEVENV;
|
||||
let server: PenpotMcpServer | undefined;
|
||||
try {
|
||||
server = new PenpotMcpServer(false);
|
||||
assert.equal(server.hasReplServer(), false);
|
||||
} finally {
|
||||
await server?.stop();
|
||||
restoreEnv(prev, prevPorts);
|
||||
}
|
||||
});
|
||||
|
||||
test("constructor creates ReplServer when PENPOT_MCP_DEVENV is 'true'", async () => {
|
||||
const prev = process.env.PENPOT_MCP_DEVENV;
|
||||
const prevPorts = setUniqueEnv();
|
||||
process.env.PENPOT_MCP_DEVENV = "true";
|
||||
let server: PenpotMcpServer | undefined;
|
||||
try {
|
||||
server = new PenpotMcpServer(false);
|
||||
assert.equal(server.hasReplServer(), true);
|
||||
} finally {
|
||||
await server?.stop();
|
||||
restoreEnv(prev, prevPorts);
|
||||
}
|
||||
});
|
||||
|
||||
test("constructor creates ReplServer when PENPOT_MCP_REPL_ENABLE is 'true' without DEVENV", async () => {
|
||||
const prevDevEnv = process.env.PENPOT_MCP_DEVENV;
|
||||
const prevReplEnable = process.env.PENPOT_MCP_REPL_ENABLE;
|
||||
const prevPorts = setUniqueEnv();
|
||||
delete process.env.PENPOT_MCP_DEVENV;
|
||||
process.env.PENPOT_MCP_REPL_ENABLE = "true";
|
||||
let server: PenpotMcpServer | undefined;
|
||||
try {
|
||||
server = new PenpotMcpServer(false);
|
||||
assert.equal(server.hasReplServer(), true);
|
||||
} finally {
|
||||
await server?.stop();
|
||||
restoreEnv(prevDevEnv, prevPorts);
|
||||
restoreOrDelete("PENPOT_MCP_REPL_ENABLE", prevReplEnable);
|
||||
}
|
||||
});
|
||||
|
||||
test("constructor does not create ReplServer when PENPOT_MCP_REPL_ENABLE is 'false' even with DEVENV", async () => {
|
||||
const prevDevEnv = process.env.PENPOT_MCP_DEVENV;
|
||||
const prevReplEnable = process.env.PENPOT_MCP_REPL_ENABLE;
|
||||
const prevPorts = setUniqueEnv();
|
||||
process.env.PENPOT_MCP_DEVENV = "true";
|
||||
process.env.PENPOT_MCP_REPL_ENABLE = "false";
|
||||
let server: PenpotMcpServer | undefined;
|
||||
try {
|
||||
server = new PenpotMcpServer(false);
|
||||
assert.equal(server.hasReplServer(), false);
|
||||
} finally {
|
||||
await server?.stop();
|
||||
restoreEnv(prevDevEnv, prevPorts);
|
||||
restoreOrDelete("PENPOT_MCP_REPL_ENABLE", prevReplEnable);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────
|
||||
|
||||
function setUniqueEnv() {
|
||||
const ports = uniquePorts();
|
||||
const prevServer = process.env.PENPOT_MCP_SERVER_PORT;
|
||||
const prevWs = process.env.PENPOT_MCP_WEBSOCKET_PORT;
|
||||
const prevRepl = process.env.PENPOT_MCP_REPL_PORT;
|
||||
process.env.PENPOT_MCP_SERVER_PORT = String(ports.server);
|
||||
process.env.PENPOT_MCP_WEBSOCKET_PORT = String(ports.ws);
|
||||
process.env.PENPOT_MCP_REPL_PORT = String(ports.repl);
|
||||
return { prevServer, prevWs, prevRepl };
|
||||
}
|
||||
|
||||
function restoreEnv(
|
||||
devEnv: string | undefined,
|
||||
ports: { prevServer: string | undefined; prevWs: string | undefined; prevRepl: string | undefined }
|
||||
) {
|
||||
if (devEnv !== undefined) {
|
||||
process.env.PENPOT_MCP_DEVENV = devEnv;
|
||||
} else {
|
||||
delete process.env.PENPOT_MCP_DEVENV;
|
||||
}
|
||||
restoreOrDelete("PENPOT_MCP_SERVER_PORT", ports.prevServer);
|
||||
restoreOrDelete("PENPOT_MCP_WEBSOCKET_PORT", ports.prevWs);
|
||||
restoreOrDelete("PENPOT_MCP_REPL_PORT", ports.prevRepl);
|
||||
}
|
||||
|
||||
function restoreOrDelete(key: string, value: string | undefined) {
|
||||
if (value !== undefined) {
|
||||
process.env[key] = value;
|
||||
} else {
|
||||
delete process.env[key];
|
||||
}
|
||||
}
|
||||
@ -56,6 +56,30 @@ export class PenpotMcpServer {
|
||||
*/
|
||||
private static readonly SESSION_TIMEOUT_MINUTES = 60;
|
||||
|
||||
/**
|
||||
* Determines whether the server is running in a Penpot development
|
||||
* environment, based on the given environment variables.
|
||||
*
|
||||
* Returns ``true`` only when ``PENPOT_MCP_DEVENV`` is ``"true"``.
|
||||
*/
|
||||
public static isDevEnvEnabled(env: Record<string, string | undefined>): boolean {
|
||||
return env.PENPOT_MCP_DEVENV === "true";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the REPL server should be enabled.
|
||||
*
|
||||
* If ``PENPOT_MCP_REPL_ENABLE`` is set, its value controls the result
|
||||
* (``"true"`` enables, any other value disables). When the variable is
|
||||
* not set, the result falls back to {@link isDevEnvEnabled}.
|
||||
*/
|
||||
public static isReplEnabled(env: Record<string, string | undefined>): boolean {
|
||||
if (env.PENPOT_MCP_REPL_ENABLE !== undefined) {
|
||||
return env.PENPOT_MCP_REPL_ENABLE === "true";
|
||||
}
|
||||
return PenpotMcpServer.isDevEnvEnabled(env);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a short, non-reversible fingerprint of a user token, suitable for
|
||||
* correlating log lines without exposing the full credential.
|
||||
@ -83,7 +107,7 @@ export class PenpotMcpServer {
|
||||
public readonly configLoader: ConfigurationLoader;
|
||||
private app: any;
|
||||
public readonly pluginBridge: PluginBridge;
|
||||
private readonly replServer: ReplServer;
|
||||
private readonly replServer: ReplServer | null;
|
||||
private apiDocs: ApiDocs;
|
||||
private readonly penpotHighLevelOverview: string;
|
||||
private readonly connectionInstructions: string;
|
||||
@ -149,7 +173,12 @@ export class PenpotMcpServer {
|
||||
}
|
||||
|
||||
this.pluginBridge = new PluginBridge(this, this.webSocketPort, toolTimeoutSecs, this.redisBridge);
|
||||
this.replServer = new ReplServer(this.pluginBridge, this.replPort, this.host);
|
||||
|
||||
if (PenpotMcpServer.isReplEnabled(process.env)) {
|
||||
this.replServer = new ReplServer(this.pluginBridge, this.replPort, this.host);
|
||||
} else {
|
||||
this.replServer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -190,7 +219,18 @@ export class PenpotMcpServer {
|
||||
* additional developer tools such as ClojureScript expression evaluation are exposed.
|
||||
*/
|
||||
public isDevEnv(): boolean {
|
||||
return process.env.PENPOT_MCP_DEVENV === "true";
|
||||
return PenpotMcpServer.isDevEnvEnabled(process.env);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the REPL server was created.
|
||||
*
|
||||
* The REPL server is created when {@link isReplEnabled} returns true,
|
||||
* which means either ``PENPOT_MCP_REPL_ENABLE=true`` or, when that
|
||||
* variable is unset, ``PENPOT_MCP_DEVENV=true``.
|
||||
*/
|
||||
public hasReplServer(): boolean {
|
||||
return this.replServer !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -421,8 +461,14 @@ export class PenpotMcpServer {
|
||||
this.logger.info(`Legacy SSE endpoint: http://${this.host}:${this.port}/sse`);
|
||||
this.logger.info(`WebSocket server URL: ws://${this.host}:${this.webSocketPort}`);
|
||||
|
||||
// start the REPL server and session timeout checker
|
||||
await this.replServer.start();
|
||||
// start the REPL server (devenv only) and session timeout checker
|
||||
if (this.replServer) {
|
||||
await this.replServer.start();
|
||||
} else {
|
||||
this.logger.info(
|
||||
"REPL server disabled (set PENPOT_MCP_REPL_ENABLE=true or PENPOT_MCP_DEVENV=true to enable)"
|
||||
);
|
||||
}
|
||||
this.startSessionTimeoutChecker();
|
||||
|
||||
resolve();
|
||||
@ -438,8 +484,11 @@ export class PenpotMcpServer {
|
||||
public async stop(): Promise<void> {
|
||||
this.logger.info("Stopping Penpot MCP Server...");
|
||||
clearInterval(this.sessionTimeoutInterval);
|
||||
await this.pluginBridge.close();
|
||||
await this.redisBridge?.close();
|
||||
await this.replServer.stop();
|
||||
if (this.replServer) {
|
||||
await this.replServer.stop();
|
||||
}
|
||||
this.logger.info("Penpot MCP Server stopped");
|
||||
}
|
||||
}
|
||||
|
||||
@ -467,4 +467,16 @@ export class PluginBridge {
|
||||
task.rejectWithError(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the WebSocket server and all connected client sockets.
|
||||
*/
|
||||
public async close(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this.wsServer.close(() => {
|
||||
this.logger.info("WebSocket server closed");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
1046
mcp/pnpm-lock.yaml
generated
1046
mcp/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,3 @@
|
||||
# auto-confirm node_modules purge when pnpm detects an incompatible modules
|
||||
# directory (e.g. after a store location or pnpm major version change),
|
||||
# preventing the interactive prompt from blocking bootstrap
|
||||
confirmModulesPurge: false
|
||||
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
sharp: false
|
||||
@ -19,9 +14,9 @@ minimumReleaseAgeExclude:
|
||||
- hono@4.12.34
|
||||
|
||||
packages:
|
||||
- "./packages/common"
|
||||
- "./packages/server"
|
||||
- "./packages/plugin"
|
||||
- "packages/common"
|
||||
- "packages/server"
|
||||
- "packages/plugin"
|
||||
|
||||
overrides:
|
||||
'@hono/node-server@<2.0.5': ^2.0.5
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
"fmt:check": "prettier --check src/ test/",
|
||||
"clean": "rm -rf dist/"
|
||||
},
|
||||
"packageManager": "pnpm@11.18.0+sha512.33d83c77da82f49fba836925c6f1b841181ec3132b670639bd012f7075f5c7cf634c5f870147c19aae7478fac01df09d8892e880454896edd23ee9b33757563c",
|
||||
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||
"dependencies": {
|
||||
"express": "^5.2.1",
|
||||
"multer": "^2.2.0",
|
||||
@ -22,19 +22,19 @@
|
||||
"pino": "^10.3.1",
|
||||
"pino-loki": "^3.0.0",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"sharp": "^0.35.3",
|
||||
"sharp": "^0.35.4",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^26.1.2",
|
||||
"@types/node": "^26.3.0",
|
||||
"@types/supertest": "^7.2.1",
|
||||
"esbuild": "^0.28.1",
|
||||
"esbuild": "^0.28.2",
|
||||
"prettier": "^3.6.2",
|
||||
"supertest": "^7.2.2",
|
||||
"tsx": "^4.22.4",
|
||||
"tsx": "^4.23.12",
|
||||
"typescript": "^7.0.2",
|
||||
"vitest": "^4.1.10"
|
||||
"vitest": "^4.1.11"
|
||||
}
|
||||
}
|
||||
|
||||
709
media-processor/pnpm-lock.yaml
generated
709
media-processor/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -13,5 +13,5 @@
|
||||
"lint": "eslint .",
|
||||
"test": "vitest"
|
||||
},
|
||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee"
|
||||
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67"
|
||||
}
|
||||
|
||||
@ -23,8 +23,8 @@
|
||||
"playwright": "^1.62.1",
|
||||
"prettier": "^3.9.6",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.2.0",
|
||||
"vite": "^8.2.2",
|
||||
"vite-live-preview": "^0.4.0"
|
||||
},
|
||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee"
|
||||
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user