mirror of
https://github.com/penpot/penpot.git
synced 2026-09-08 21:19:49 +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
|
agent: build
|
||||||
---
|
---
|
||||||
|
|
||||||
# Implement Plan
|
|
||||||
|
|
||||||
This command is run once a plan is ready (for example, from plan mode). Execute
|
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
|
the plan already prepared in the current session context. Follow these steps in order.
|
||||||
extra arguments. Follow these steps in order.
|
|
||||||
|
|
||||||
## 1. Create the issue
|
## 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 |
|
| **Low:** | Minor, optional | Author may ignore — formatting, style preferences |
|
||||||
| **Suggestion:** | Worth considering | Not required, but improves the code |
|
| **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.
|
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.
|
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
|
### 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
|
### 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
|
### Suggested Refactoring
|
||||||
|
|
||||||
@ -148,6 +150,8 @@ Choose one:
|
|||||||
- **Approve with minor changes** — Good to merge after addressing low/medium issues
|
- **Approve with minor changes** — Good to merge after addressing low/medium issues
|
||||||
- **Request changes** — Critical or high issues must be resolved before merge
|
- **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
|
## Change Sizing
|
||||||
|
|
||||||
Small, focused changes are easier to review, faster to merge, and safer to deploy.
|
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
|
## 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
|
- [ ] Tests pass — run them yourself, don't trust the claim
|
||||||
- [ ] All Required (no-prefix) changes are resolved or explicitly deferred with justification
|
|
||||||
- [ ] Tests pass
|
|
||||||
- [ ] Build succeeds
|
- [ ] Build succeeds
|
||||||
- [ ] The verification story is documented (what changed, how it was verified)
|
- [ ] The verification story is documented (what changed, how it was verified)
|
||||||
- [ ] Dependency upgrades reviewed against changelog, isolated per package, verified by green suite
|
- [ ] 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
|
## See Also
|
||||||
|
|
||||||
- For detailed security review guidance, see `security-and-hardening`
|
- For detailed security review guidance, see `security-and-hardening`
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
name: planner
|
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
|
# Planner
|
||||||
|
|
||||||
Read-only senior software architect role for Penpot. Produces structured
|
Read-only senior software architect role for Penpot. Produces structured
|
||||||
implementation plans that engineers or other agents can execute. Never writes
|
implementation plans with task breakdowns that engineers or other agents can
|
||||||
or modifies code.
|
execute. Never writes or modifies code.
|
||||||
|
|
||||||
## When to Use
|
## 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 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
|
- The user is about to start non-trivial work and wants a bite-sized task
|
||||||
breakdown.
|
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.
|
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
|
## Role
|
||||||
|
|
||||||
You are a Senior Software Architect working on Penpot, an open-source design
|
You help users understand the Penpot codebase, design solutions, and produce
|
||||||
tool. Your sole responsibility is planning and analysis — you do NOT write or
|
implementation plans that other agents or developers can execute. The plan
|
||||||
modify code.
|
tells them what to build and how to verify it, task by task.
|
||||||
|
|
||||||
You help users understand the codebase, design solutions, and create detailed
|
The implementer reads the project's agent docs (`AGENTS.md`, project memories
|
||||||
implementation plans that other agents or developers can execute. Document
|
such as `mem:critical-info`, `mem:testing`, and each module's core memory)
|
||||||
everything they need to know: which files to touch for each task, code patterns,
|
before working. Reference those memories instead of re-explaining tooling,
|
||||||
tests, and how to verify correctness. Apply DRY and KISS principles.
|
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
|
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:
|
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.
|
Skipping this step is the #1 cause of incorrect or incomplete plans.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## The Planning Process
|
## The Planning Process
|
||||||
|
|
||||||
### Phase 1: Architecture Analysis
|
### Phase 1: Architecture Analysis
|
||||||
@ -64,16 +71,42 @@ Skipping this step is the #1 cause of incorrect or incomplete plans.
|
|||||||
|
|
||||||
### Phase 2: Task Breakdown
|
### Phase 2: Task Breakdown
|
||||||
|
|
||||||
Implementation order follows the monorepo's dependency graph:
|
#### Identify the Dependency Graph
|
||||||
`frontend -> common`, `backend -> common`, `exporter -> common`,
|
|
||||||
`frontend -> render-wasm`. Build shared foundations first, then layer
|
Map what depends on what, following the monorepo's module dependency graph:
|
||||||
consumers on top.
|
|
||||||
|
```
|
||||||
|
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
|
#### Slice Vertically
|
||||||
|
|
||||||
Instead of building all of common, then all of backend, then all of frontend —
|
Instead of building all of common, then all of backend, then all of frontend —
|
||||||
build one complete feature path at a time:
|
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 1: common data types + schema ← foundation
|
||||||
Task 2: backend RPC handler + persistence
|
Task 2: backend RPC handler + persistence
|
||||||
@ -89,39 +122,58 @@ Each task follows this structure:
|
|||||||
```markdown
|
```markdown
|
||||||
## Task [N]: [Short descriptive title]
|
## 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:**
|
**Acceptance criteria:**
|
||||||
- [ ] [Specific, testable condition]
|
- [ ] [Specific, testable condition]
|
||||||
- [ ] [Specific, testable condition]
|
- [ ] [Specific, testable condition]
|
||||||
|
|
||||||
**Verification:**
|
**Verification:**
|
||||||
- [ ] Tests pass (module-specific test command)
|
- [ ] Relevant tests pass (module-specific test command).
|
||||||
- [ ] Lint/formatter passes (module-specific check 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"]
|
**Dependencies:** [Task numbers this depends on, or "None"]
|
||||||
|
|
||||||
**Files likely touched:**
|
**Files likely touched:**
|
||||||
- `path/to/file.clj`
|
- `path/to/file.clj`
|
||||||
- `path/to/file_test.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
|
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,
|
(e.g. `clojure -M:dev:test` for backend/common,
|
||||||
or the commands noted in the module's core memory).
|
`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
|
#### Estimate Scope
|
||||||
|
|
||||||
| Size | Files | Scope |
|
| Size | Files | Scope | Example |
|
||||||
|------|-------|-------|
|
|------|-------|-------|---------|
|
||||||
| **XS** | 1 | Single function, config change, or schema tweak |
|
| **XS** | 1 | Single function, config change, or schema tweak | Add a validation rule |
|
||||||
| **S** | 1-2 | One handler or component method |
|
| **S** | 1-2 | One handler or component method | Add a new RPC endpoint |
|
||||||
| **M** | 3-5 | One vertical feature slice |
|
| **M** | 3-5 | One vertical feature slice | Bookmark CRUD with tests |
|
||||||
| **L** | 5-8 | Multi-component feature |
|
| **L** | 5-8 | Multi-component feature | Search with filtering and pagination |
|
||||||
| **XL** | 8+ | **Too large — break it down further** |
|
| **XL** | 8+ | **Too large — break it down further** | — |
|
||||||
|
|
||||||
If a task is L or larger, break it into smaller tasks. Agents perform best on
|
If a task is XL, it should be broken into smaller tasks. Agents perform best
|
||||||
S and M tasks.
|
on S and M tasks.
|
||||||
|
|
||||||
**When to break a task down further:**
|
**When to break a task down further:**
|
||||||
- It would take more than one focused session
|
- It would take more than one focused session
|
||||||
@ -141,11 +193,11 @@ Arrange tasks so that:
|
|||||||
Add explicit checkpoints with the relevant module commands:
|
Add explicit checkpoints with the relevant module commands:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
## Checkpoint: After Tasks 1-3
|
### Checkpoint: After Tasks 1-3
|
||||||
- [ ] All tests pass (module-specific command)
|
- [ ] Relevant tests pass (module-specific command).
|
||||||
- [ ] Lint/format passes (module-specific command)
|
- [ ] The relevant build or compilation passes, if applicable.
|
||||||
- [ ] Core flow works end-to-end
|
- [ ] The core flow works end-to-end.
|
||||||
- [ ] Review with human before proceeding
|
- [ ] Review with human before proceeding.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@ -159,7 +211,7 @@ Add explicit checkpoints with the relevant module commands:
|
|||||||
- Apply DRY and KISS principles to the proposed implementation.
|
- Apply DRY and KISS principles to the proposed implementation.
|
||||||
- Define a testing strategy aligned with each affected module's tooling.
|
- Define a testing strategy aligned with each affected module's tooling.
|
||||||
- Every task must have acceptance criteria and verification steps.
|
- Every task must have acceptance criteria and verification steps.
|
||||||
- Checkpoints must exist between major phases.
|
- Checkpoints must exist after every 2-3 tasks.
|
||||||
|
|
||||||
## Constraints
|
## Constraints
|
||||||
|
|
||||||
@ -168,7 +220,8 @@ Add explicit checkpoints with the relevant module commands:
|
|||||||
`.opencode/plans/`.
|
`.opencode/plans/`.
|
||||||
- You do **not** run builds, tests, linters, or any commands that modify state.
|
- 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** 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
|
- Your output is a structured plan or analysis, ready for handoff to an
|
||||||
engineer agent or developer.
|
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
|
(e.g. `add-batch-get-profiles-for-file-comments`). Create the
|
||||||
`.opencode/plans/` directory if it does not exist.
|
`.opencode/plans/` directory if it does not exist.
|
||||||
|
|
||||||
Always attempt the write. If the user explicitly provides a target file path,
|
IMPORTANT: The plan agent has write permission specifically for
|
||||||
use that path instead of the default.
|
`.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
|
### Plan Document Template
|
||||||
|
|
||||||
@ -212,41 +266,75 @@ use that path instead of the default.
|
|||||||
security implications.]
|
security implications.]
|
||||||
|
|
||||||
## Approach
|
## Approach
|
||||||
[Step-by-step implementation plan with file paths, function names, and code
|
[A short strategy summary: 3-5 sentences describing the overall approach and
|
||||||
shape where applicable. Group steps into atomic, ordered tasks.]
|
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
|
## Task List
|
||||||
|
|
||||||
### Phase 1: Foundation
|
Each task uses the full task structure defined in
|
||||||
- [ ] Task 1: ...
|
[Write Tasks](#write-tasks) — description, rationale, acceptance criteria,
|
||||||
- [ ] Task 2: ...
|
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
|
Tasks are a flat, ordered list — a plan is not a roadmap. Do not group tasks
|
||||||
- [ ] Tests pass, lint/formatter clean (module-specific commands)
|
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 1: [Short descriptive title]
|
||||||
- [ ] Task 3: ...
|
|
||||||
- [ ] Task 4: ...
|
|
||||||
|
|
||||||
### Checkpoint: Phase 2
|
**Description:** [What this task accomplishes.]
|
||||||
- [ ] End-to-end flow works
|
|
||||||
|
|
||||||
### Phase 3: Polish
|
**Rationale:** [Why this approach over the alternatives.]
|
||||||
- [ ] Task 5: ...
|
|
||||||
- [ ] Task 6: ...
|
|
||||||
|
|
||||||
### Checkpoint: Complete
|
**Acceptance criteria:**
|
||||||
- [ ] All acceptance criteria met
|
- [ ] [Specific, testable condition]
|
||||||
- [ ] Ready for review
|
|
||||||
|
|
||||||
## Testing Strategy
|
**Verification:**
|
||||||
[How to verify: which test commands to run per module, what cases to cover,
|
- [ ] Relevant tests pass (module-specific command).
|
||||||
manual verification steps, lint/format checks. Consult each module's core
|
|
||||||
memory for the exact commands.]
|
**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
|
## Parallelization Opportunities
|
||||||
- **Safe to parallelize:** Independent feature slices across separate
|
- **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
|
- **Must be sequential:** Shared common schema changes, database migrations
|
||||||
- **Needs coordination:** Features that share a contract (define the contract
|
- **Needs coordination:** Features that share a contract (define the contract
|
||||||
first, then parallelize)
|
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
|
with no implementation), skip the **Approach** and **Task List** sections and
|
||||||
lead with **Findings** instead, keeping the rest of the structure.
|
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
|
## Verification Checklist
|
||||||
|
|
||||||
Before starting implementation, confirm:
|
Before delivering the plan, confirm:
|
||||||
|
|
||||||
- [ ] Every task has acceptance criteria
|
- [ ] Every task has acceptance criteria
|
||||||
- [ ] Every task has a verification step
|
- [ ] Every task has a verification step
|
||||||
- [ ] Task dependencies are identified and ordered correctly
|
- [ ] Task dependencies are identified and ordered correctly
|
||||||
- [ ] No task touches more than ~5 files
|
- [ ] No task is XL or larger — break it down instead
|
||||||
- [ ] Checkpoints exist between major phases
|
- [ ] Checkpoints exist after every 2-3 tasks
|
||||||
- [ ] The human has reviewed and approved the plan
|
- [ ] 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
|
release, but the PR is being released elsewhere — the fix may not
|
||||||
actually ship here.
|
actually ship here.
|
||||||
2. **PR is in the milestone, but the issue it closes is in a different
|
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
|
milestone.** The PR is being released here, but the issue it fixes is
|
||||||
the issue it fixes is being released in a different version (or never
|
being released in a different version — the changelog pairing is
|
||||||
tracked in a milestone) — the changelog pairing is misleading.
|
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
|
**Anything else is not an anomaly.** Other discrepancies (exclusion
|
||||||
labels on in-changelog issues, missing valid issues, unmerged PR
|
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
|
if get_pr_milestone(pr_num) != MILESTONE: continue
|
||||||
for issue_num in pr.get('closing_issues', []):
|
for issue_num in pr.get('closing_issues', []):
|
||||||
issue_ms = get_issue_milestone(issue_num)
|
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:
|
if issue_ms != MILESTONE:
|
||||||
anomalies_b.append({
|
anomalies_b.append({
|
||||||
'pr': pr_num,
|
'pr': pr_num,
|
||||||
@ -657,7 +667,7 @@ with open(OUTPUT, 'w') as f:
|
|||||||
|
|
||||||
f.write('## Summary\n\n')
|
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'- **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')
|
f.write(f'- **Total anomalies:** {n_a + n_b}\n\n')
|
||||||
|
|
||||||
# --- Anomalies section ---
|
# --- Anomalies section ---
|
||||||
@ -686,7 +696,7 @@ with open(OUTPUT, 'w') as f:
|
|||||||
f.write('\n')
|
f.write('\n')
|
||||||
|
|
||||||
if n_b:
|
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 = {}
|
by_pr = {}
|
||||||
for b in anomalies_b:
|
for b in anomalies_b:
|
||||||
by_pr.setdefault(b['pr'], []).append(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** —
|
1. **Issue in milestone, referenced PR in different milestone or no milestone** —
|
||||||
the changelog claims a fix here, but the PR is released elsewhere.
|
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.
|
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
|
**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).
|
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
|
- **Anomaly = milestone mismatch only.** The report contains only milestone
|
||||||
mismatches: (1) the issue is in this milestone but the referenced PR is
|
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
|
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
|
milestone but the issue it closes is in a different milestone. An
|
||||||
unassigned). These are anomalies because the changelog pairing is
|
*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
|
*misleading* — the human needs to decide whether the milestone or the
|
||||||
changelog is wrong. All other discrepancies (exclusion labels, missing
|
changelog is wrong. All other discrepancies (exclusion labels, missing
|
||||||
valid issues, unmerged PR references, duplicates, stale milestone
|
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-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.
|
- `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.
|
- All projects join external network `penpot_shared`. Created idempotently by `ensure-devenv-network`, never removed by lifecycle commands.
|
||||||
|
|
||||||
## Source-of-truth files
|
## Source-of-truth files
|
||||||
@ -65,7 +66,7 @@ No `--delete` on the working-tree pass: gitignored caches in the workspace survi
|
|||||||
|
|
||||||
## CLI surface
|
## 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.
|
- `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.
|
- `run-devenv`: legacy alias, ws0 non-agentic attached.
|
||||||
- `attach-devenv [--ws main|0|wsN|N]`: pure attach. Fails fast if instance/session missing.
|
- `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 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 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))
|
- 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 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
|
### :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))
|
- 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))
|
- 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))
|
- 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))
|
- 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))
|
- 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
|
## 2.17.2
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
### :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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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.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
|
;; Minimal dependencies required by lettuce, we need to include them
|
||||||
;; explicitly because clojure dependency management does not support
|
;; explicitly because clojure dependency management does not support
|
||||||
;; yet the BOM format.
|
;; yet the BOM format.
|
||||||
@ -25,7 +25,7 @@
|
|||||||
io.micrometer/micrometer-observation {:mvn/version "1.14.2"}
|
io.micrometer/micrometer-observation {:mvn/version "1.14.2"}
|
||||||
|
|
||||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
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
|
funcool/yetti
|
||||||
{:git/tag "v11.10"
|
{:git/tag "v11.10"
|
||||||
@ -40,32 +40,32 @@
|
|||||||
nrepl/nrepl {:mvn/version "1.7.0"}
|
nrepl/nrepl {:mvn/version "1.7.0"}
|
||||||
|
|
||||||
org.postgresql/postgresql {:mvn/version "42.7.13"}
|
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"}
|
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-hashers {:mvn/version "2.0.167"}
|
||||||
buddy/buddy-sign {:mvn/version "3.6.1-359"}
|
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"}
|
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
|
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"}
|
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
|
||||||
|
|
||||||
dawran6/emoji {:mvn/version "0.2.0"}
|
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 Print specs
|
||||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||||
software.amazon.awssdk/s3 {:mvn/version "2.50.1"}
|
software.amazon.awssdk/s3 {:mvn/version "2.54.5"}
|
||||||
software.amazon.awssdk/sts {:mvn/version "2.50.1"}
|
software.amazon.awssdk/sts {:mvn/version "2.54.5"}
|
||||||
|
|
||||||
com.ladybugdb/lbug {:mvn/version "0.19.1"}
|
com.ladybugdb/lbug {:mvn/version "0.19.1"}
|
||||||
;; Required by Arrow RootAllocator (lbug only pulls arrow-memory-core).
|
;; Required by Arrow RootAllocator (lbug only pulls arrow-memory-core).
|
||||||
|
|||||||
@ -14,10 +14,21 @@
|
|||||||
:iterations 3
|
:iterations 3
|
||||||
:parallelism 2})
|
:parallelism 2})
|
||||||
|
|
||||||
|
(def ^:private weak-options
|
||||||
|
{:alg :pbkdf2+sha256
|
||||||
|
:iterations 100})
|
||||||
|
|
||||||
(defn derive-password
|
(defn derive-password
|
||||||
[password]
|
[password]
|
||||||
(hashers/derive password default-options))
|
(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
|
(defn verify-password
|
||||||
[attempt password]
|
[attempt password]
|
||||||
(try
|
(try
|
||||||
|
|||||||
@ -9,7 +9,9 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex])
|
[app.common.exceptions :as ex])
|
||||||
(:import
|
(: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
|
(defonce ^:private passay-code->translation-key
|
||||||
{"INSUFFICIENT_LOWERCASE" "errors.weak-password.insufficient-lowercase"
|
{"INSUFFICIENT_LOWERCASE" "errors.weak-password.insufficient-lowercase"
|
||||||
@ -18,12 +20,13 @@
|
|||||||
"INSUFFICIENT_SPECIAL" "errors.weak-password.insufficient-special"})
|
"INSUFFICIENT_SPECIAL" "errors.weak-password.insufficient-special"})
|
||||||
|
|
||||||
(defonce ^:private character-characteristics-rule
|
(defonce ^:private character-characteristics-rule
|
||||||
(doto (CharacterCharacteristicsRule.)
|
(CharacterCharacteristicsRule.
|
||||||
(.setRules [(CharacterRule. EnglishCharacterData/LowerCase 1)
|
4
|
||||||
|
(into-array org.passay.rule.CharacterRule
|
||||||
|
[(CharacterRule. EnglishCharacterData/LowerCase 1)
|
||||||
(CharacterRule. EnglishCharacterData/UpperCase 1)
|
(CharacterRule. EnglishCharacterData/UpperCase 1)
|
||||||
(CharacterRule. EnglishCharacterData/Digit 1)
|
(CharacterRule. EnglishCharacterData/Digit 1)
|
||||||
(CharacterRule. EnglishCharacterData/Special 1)])
|
(CharacterRule. EnglishCharacterData/Special 1)])))
|
||||||
(.setNumberOfCharacteristics 4)))
|
|
||||||
|
|
||||||
(defn validate-password
|
(defn validate-password
|
||||||
"Validates password strength.
|
"Validates password strength.
|
||||||
|
|||||||
@ -42,6 +42,7 @@
|
|||||||
[datoteka.io :as io])
|
[datoteka.io :as io])
|
||||||
(:import
|
(:import
|
||||||
java.io.File
|
java.io.File
|
||||||
|
java.io.FilterInputStream
|
||||||
java.io.InputStream
|
java.io.InputStream
|
||||||
java.io.OutputStreamWriter
|
java.io.OutputStreamWriter
|
||||||
java.lang.AutoCloseable
|
java.lang.AutoCloseable
|
||||||
@ -461,6 +462,31 @@
|
|||||||
[^ZipFile input ^ZipEntry entry]
|
[^ZipFile input ^ZipEntry entry]
|
||||||
(.getInputStream input 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
|
(defn- zip-entry-reader
|
||||||
[^ZipFile input ^ZipEntry entry]
|
[^ZipFile input ^ZipEntry entry]
|
||||||
(-> (zip-entry-stream input entry)
|
(-> (zip-entry-stream input entry)
|
||||||
@ -469,10 +495,12 @@
|
|||||||
(defn- zip-entry-storage-content
|
(defn- zip-entry-storage-content
|
||||||
"Wraps a ZipFile and ZipEntry into a penpot storage compatible
|
"Wraps a ZipFile and ZipEntry into a penpot storage compatible
|
||||||
object and avoid creating temporal objects"
|
object and avoid creating temporal objects"
|
||||||
[input entry]
|
[input entry & {:keys [max-size]}]
|
||||||
(let [hash (delay (->> entry
|
(let [stream-fn (fn []
|
||||||
(zip-entry-stream input)
|
(cond-> (zip-entry-stream input entry)
|
||||||
(sto.impl/calculate-hash)))]
|
max-size (size-limiting-stream max-size)))
|
||||||
|
hash (delay (->> (stream-fn)
|
||||||
|
(sto.impl/calculate-hash)))]
|
||||||
(reify
|
(reify
|
||||||
sto.impl/IContentObject
|
sto.impl/IContentObject
|
||||||
(get-size [_]
|
(get-size [_]
|
||||||
@ -489,7 +517,7 @@
|
|||||||
(throw (UnsupportedOperationException. "not implemented")))
|
(throw (UnsupportedOperationException. "not implemented")))
|
||||||
|
|
||||||
(make-input-stream [_ _]
|
(make-input-stream [_ _]
|
||||||
(zip-entry-stream input entry))
|
(stream-fn))
|
||||||
(make-output-stream [_ _]
|
(make-output-stream [_ _]
|
||||||
(throw (UnsupportedOperationException. "not implemented"))))))
|
(throw (UnsupportedOperationException. "not implemented"))))))
|
||||||
|
|
||||||
@ -884,9 +912,9 @@
|
|||||||
|
|
||||||
ext (cmedia/mtype->extension (:content-type object))
|
ext (cmedia/mtype->extension (:content-type object))
|
||||||
path (str "objects/" id ext)
|
path (str "objects/" id ext)
|
||||||
content (->> path
|
content (zip-entry-storage-content input
|
||||||
(get-zip-entry input)
|
(get-zip-entry input path)
|
||||||
(zip-entry-storage-content input))]
|
:max-size (::bfc/import-max-object-size cfg))]
|
||||||
|
|
||||||
(when (not= (:size object) (sto/get-size content))
|
(when (not= (:size object) (sto/get-size content))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
@ -896,6 +924,15 @@
|
|||||||
:expected-size (:size object)
|
:expected-size (:size object)
|
||||||
:found-size (sto/get-size content)))
|
: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-let [hash (get object :hash)]
|
||||||
(when (not= hash (sto/get-hash content))
|
(when (not= hash (sto/get-hash content))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
@ -1117,6 +1154,15 @@
|
|||||||
(let [manifest (-> (read-manifest input)
|
(let [manifest (-> (read-manifest input)
|
||||||
(validate-manifest))
|
(validate-manifest))
|
||||||
entries (read-zip-entries input)
|
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
|
cfg (-> cfg
|
||||||
(assoc ::entries entries)
|
(assoc ::entries entries)
|
||||||
(assoc ::manifest manifest)
|
(assoc ::manifest manifest)
|
||||||
|
|||||||
@ -94,7 +94,11 @@
|
|||||||
|
|
||||||
;; SSRF protection
|
;; SSRF protection
|
||||||
:ssrf-allowed-hosts #{}
|
: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
|
(def schema:config
|
||||||
(do #_sm/optional-keys
|
(do #_sm/optional-keys
|
||||||
@ -151,6 +155,10 @@
|
|||||||
[:media-processing-service-uri {:optional true} ::sm/uri]
|
[:media-processing-service-uri {:optional true} ::sm/uri]
|
||||||
[:media-processing-service-timeout {:optional true} ::sm/int]
|
[: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]
|
[:deletion-delay {:optional true} ::ct/duration]
|
||||||
[:file-clean-delay {:optional true} ::ct/duration]
|
[:file-clean-delay {:optional true} ::ct/duration]
|
||||||
[:telemetry-enabled {:optional true} ::sm/boolean]
|
[:telemetry-enabled {:optional true} ::sm/boolean]
|
||||||
|
|||||||
@ -31,8 +31,8 @@
|
|||||||
com.zaxxer.hikari.HikariDataSource
|
com.zaxxer.hikari.HikariDataSource
|
||||||
com.zaxxer.hikari.HikariPoolMXBean
|
com.zaxxer.hikari.HikariPoolMXBean
|
||||||
com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory
|
com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory
|
||||||
io.whitfin.siphash.SipHasher
|
io.whitfin.siphash.SipHash
|
||||||
io.whitfin.siphash.SipHasherContainer
|
io.whitfin.siphash.SipHashContext
|
||||||
java.io.InputStream
|
java.io.InputStream
|
||||||
java.io.OutputStream
|
java.io.OutputStream
|
||||||
java.sql.Connection
|
java.sql.Connection
|
||||||
@ -701,12 +701,12 @@
|
|||||||
;; --- Locks
|
;; --- Locks
|
||||||
|
|
||||||
(def ^:private siphash-state
|
(def ^:private siphash-state
|
||||||
(SipHasher/container
|
(SipHash/context
|
||||||
(uuid/get-bytes uuid/zero)))
|
(uuid/get-bytes uuid/zero)))
|
||||||
|
|
||||||
(defn uuid->hash-code
|
(defn uuid->hash-code
|
||||||
[o]
|
[o]
|
||||||
(.hash ^SipHasherContainer siphash-state
|
(.hash ^SipHashContext siphash-state
|
||||||
^bytes (uuid/get-bytes o)))
|
^bytes (uuid/get-bytes o)))
|
||||||
|
|
||||||
(defn- xact-check-param
|
(defn- xact-check-param
|
||||||
|
|||||||
@ -370,7 +370,9 @@
|
|||||||
::bfc/overwrite false
|
::bfc/overwrite false
|
||||||
::bfc/profile-id profile-id
|
::bfc/profile-id profile-id
|
||||||
::bfc/project-id project-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)
|
(bf.v3/import-files! cfg)
|
||||||
{::yres/status 200
|
{::yres/status 200
|
||||||
::yres/headers {"content-type" "text/plain"}
|
::yres/headers {"content-type" "text/plain"}
|
||||||
@ -626,7 +628,9 @@
|
|||||||
::bfc/profile-id profile-id
|
::bfc/profile-id profile-id
|
||||||
::bfc/project-id project-id
|
::bfc/project-id project-id
|
||||||
::bfc/input path
|
::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)
|
(if (= format :binfile-v3)
|
||||||
(bf.v3/import-files! cfg)
|
(bf.v3/import-files! cfg)
|
||||||
|
|||||||
@ -400,6 +400,8 @@
|
|||||||
|
|
||||||
:delete-object
|
:delete-object
|
||||||
(ig/ref :app.tasks.delete-object/handler)
|
(ig/ref :app.tasks.delete-object/handler)
|
||||||
|
:demo-purge
|
||||||
|
(ig/ref :app.tasks.demo-purge/handler)
|
||||||
:process-webhook-event
|
:process-webhook-event
|
||||||
(ig/ref ::webhooks/process-event-handler)
|
(ig/ref ::webhooks/process-event-handler)
|
||||||
:run-webhook
|
:run-webhook
|
||||||
@ -437,6 +439,9 @@
|
|||||||
:app.tasks.delete-object/handler
|
:app.tasks.delete-object/handler
|
||||||
{::db/pool (ig/ref ::db/pool)}
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
|
|
||||||
|
:app.tasks.demo-purge/handler
|
||||||
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
|
|
||||||
:app.tasks.file-gc/handler
|
:app.tasks.file-gc/handler
|
||||||
{::db/pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)
|
||||||
::sto/storage (ig/ref ::sto/storage)}
|
::sto/storage (ig/ref ::sto/storage)}
|
||||||
|
|||||||
@ -103,7 +103,9 @@
|
|||||||
(assoc ::bfc/project-id project-id)
|
(assoc ::bfc/project-id project-id)
|
||||||
(assoc ::bfc/profile-id profile-id)
|
(assoc ::bfc/profile-id profile-id)
|
||||||
(assoc ::bfc/team-id (:id team))
|
(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)
|
input-path (:path file)
|
||||||
owned? (some? upload-id)
|
owned? (some? upload-id)
|
||||||
|
|||||||
@ -231,8 +231,11 @@
|
|||||||
::sm/params schema:get-comment-threads}
|
::sm/params schema:get-comment-threads}
|
||||||
[cfg {:keys [::rpc/profile-id file-id share-id] :as params}]
|
[cfg {:keys [::rpc/profile-id file-id share-id] :as params}]
|
||||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
(let [perms (files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||||
(get-comment-threads conn profile-id file-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
|
(defn- get-comment-threads-sql
|
||||||
[where]
|
[where]
|
||||||
@ -329,9 +332,15 @@
|
|||||||
::sm/params schema:get-comment-thread}
|
::sm/params schema:get-comment-thread}
|
||||||
[cfg {:keys [::rpc/profile-id file-id id share-id] :as params}]
|
[cfg {:keys [::rpc/profile-id file-id id share-id] :as params}]
|
||||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
(let [perms (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])
|
thread (some-> (db/exec-one! conn [sql:get-comment-thread profile-id file-id id])
|
||||||
(decode-row)))))
|
(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
|
;; --- COMMAND: Retrieve Comments
|
||||||
|
|
||||||
@ -348,8 +357,13 @@
|
|||||||
::sm/params schema:get-comments}
|
::sm/params schema:get-comments}
|
||||||
[cfg {:keys [::rpc/profile-id thread-id share-id]}]
|
[cfg {:keys [::rpc/profile-id thread-id share-id]}]
|
||||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||||
(let [{:keys [file-id]} (get-comment-thread conn thread-id)]
|
(let [{:keys [file-id page-id]} (get-comment-thread conn thread-id)
|
||||||
(files/check-comment-permissions! cfg profile-id file-id share-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)))))
|
(get-comments conn thread-id)))))
|
||||||
|
|
||||||
(def sql:get-comments
|
(def sql:get-comments
|
||||||
|
|||||||
@ -7,9 +7,9 @@
|
|||||||
(ns app.rpc.commands.demo
|
(ns app.rpc.commands.demo
|
||||||
"A demo specific mutations."
|
"A demo specific mutations."
|
||||||
(:require
|
(:require
|
||||||
[app.auth :refer [derive-password]]
|
[app.auth :refer [derive-password-weak]]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.time :as ct]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
@ -17,6 +17,7 @@
|
|||||||
[app.rpc.commands.auth :as auth]
|
[app.rpc.commands.auth :as auth]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
|
[app.worker :as wrk]
|
||||||
[buddy.core.codecs :as bc]
|
[buddy.core.codecs :as bc]
|
||||||
[buddy.core.nonce :as bn]))
|
[buddy.core.nonce :as bn]))
|
||||||
|
|
||||||
@ -34,8 +35,8 @@
|
|||||||
:code :demo-users-not-allowed
|
:code :demo-users-not-allowed
|
||||||
:hint "Demo users are disabled by config."))
|
:hint "Demo users are disabled by config."))
|
||||||
|
|
||||||
(let [sem (System/currentTimeMillis)
|
(let [sem (uuid/next)
|
||||||
email (str "demo-" sem ".demo@example.com")
|
email (str "demo-" sem "@demo.example.com")
|
||||||
fullname (str "Demo User " sem)
|
fullname (str "Demo User " sem)
|
||||||
|
|
||||||
password (-> (bn/random-bytes 16)
|
password (-> (bn/random-bytes 16)
|
||||||
@ -46,13 +47,17 @@
|
|||||||
:fullname fullname
|
:fullname fullname
|
||||||
:is-active true
|
:is-active true
|
||||||
:is-demo true
|
:is-demo true
|
||||||
:deleted-at (ct/in-future (cf/get-deletion-delay))
|
:password (derive-password-weak password)
|
||||||
:password (derive-password password)
|
|
||||||
:props {}}
|
:props {}}
|
||||||
profile (db/tx-run! cfg (fn [cfg]
|
profile (db/tx-run! cfg (fn [cfg]
|
||||||
(->> (auth/create-profile cfg params)
|
(->> (auth/create-profile cfg params)
|
||||||
(auth/create-profile-rels cfg))))]
|
(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
|
(with-meta {:email email
|
||||||
:password password}
|
:password password}
|
||||||
{::audit/profile-id (:id profile)})))
|
{::audit/profile-id (:id profile)})))
|
||||||
|
|
||||||
|
|||||||
@ -95,18 +95,23 @@
|
|||||||
(def check-read-permissions!
|
(def check-read-permissions!
|
||||||
(perms/make-check-fn has-read-permissions?))
|
(perms/make-check-fn has-read-permissions?))
|
||||||
|
|
||||||
;; A user has comment permissions if she has read permissions, or
|
;; A user has comment permissions if:
|
||||||
;; explicit comment permissions through the share-id
|
;; - 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!
|
(defn check-comment-permissions!
|
||||||
[cfg profile-id file-id share-id]
|
[cfg profile-id file-id share-id]
|
||||||
(let [perms (perms/get-file-read-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)
|
allowed? (if (= :share-link (:type perms))
|
||||||
can-comment (has-comment-permissions? perms)]
|
(has-comment-permissions? perms)
|
||||||
(when-not (or can-read can-comment)
|
(or (has-read-permissions? perms)
|
||||||
|
(has-comment-permissions? perms)))]
|
||||||
|
(when-not allowed?
|
||||||
(ex/raise :type :not-found
|
(ex/raise :type :not-found
|
||||||
:code :object-not-found
|
:code :object-not-found
|
||||||
:hint "not found"))))
|
:hint "not found"))
|
||||||
|
perms))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; QUERY COMMANDS
|
;; QUERY COMMANDS
|
||||||
|
|||||||
@ -426,7 +426,9 @@
|
|||||||
(assoc ::bfc/project-id project-id)
|
(assoc ::bfc/project-id project-id)
|
||||||
(assoc ::bfc/profile-id profile-id)
|
(assoc ::bfc/profile-id profile-id)
|
||||||
(assoc ::bfc/input template)
|
(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)
|
result (if (= format :binfile-v3)
|
||||||
(bf.v3/import-files! cfg)
|
(bf.v3/import-files! cfg)
|
||||||
|
|||||||
@ -141,9 +141,7 @@
|
|||||||
(defn get-profile
|
(defn get-profile
|
||||||
"Get profile by id. Throws not-found exception if no profile found."
|
"Get profile by id. Throws not-found exception if no profile found."
|
||||||
[conn id & {:as opts}]
|
[conn id & {:as opts}]
|
||||||
;; NOTE: We need to set ::db/remove-deleted to false because demo profiles
|
(-> (db/get-by-id conn :profile id opts)
|
||||||
;; are created with a set deleted-at value
|
|
||||||
(-> (db/get-by-id conn :profile id (assoc opts ::db/remove-deleted false))
|
|
||||||
(decode-row)))
|
(decode-row)))
|
||||||
|
|
||||||
;; --- MUTATION: Update Profile (own)
|
;; --- MUTATION: Update Profile (own)
|
||||||
|
|||||||
@ -944,8 +944,10 @@
|
|||||||
::sm/params schema:delete-team-member
|
::sm/params schema:delete-team-member
|
||||||
::db/transaction true}
|
::db/transaction true}
|
||||||
[{:keys [::db/conn ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}]
|
[{: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)
|
(let [team (get-team conn :profile-id profile-id :team-id team-id)
|
||||||
perms (get-permissions conn profile-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)
|
(when-not (or (:is-owner perms)
|
||||||
(:is-admin perms))
|
(:is-admin perms))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
@ -955,6 +957,15 @@
|
|||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :cant-remove-yourself))
|
: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
|
(db/delete! conn :team-profile-rel {:profile-id member-id
|
||||||
:team-id team-id})
|
:team-id team-id})
|
||||||
|
|
||||||
|
|||||||
@ -56,7 +56,7 @@
|
|||||||
(assoc :can-read true)))
|
(assoc :can-read true)))
|
||||||
|
|
||||||
(defn- get-view-only-bundle
|
(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)
|
(let [file (bfc/get-file cfg file-id)
|
||||||
|
|
||||||
project (db/get conn :project
|
project (db/get conn :project
|
||||||
@ -89,16 +89,18 @@
|
|||||||
(mapv (fn [{:keys [id] :as lib}]
|
(mapv (fn [{:keys [id] :as lib}]
|
||||||
(merge lib (bfc/get-file cfg id)))))
|
(merge lib (bfc/get-file cfg id)))))
|
||||||
|
|
||||||
links (->> (db/query conn :share-link {:file-id file-id})
|
links (cond->> (->> (db/query conn :share-link {:file-id file-id})
|
||||||
(mapv (fn [row]
|
(mapv (fn [row]
|
||||||
(-> row
|
(-> row
|
||||||
(update :pages db/decode-pgarray #{})
|
(update :pages db/decode-pgarray #{})
|
||||||
;; NOTE: the flags are deprecated but are still present
|
;; NOTE: the flags are deprecated but are still present
|
||||||
;; on the table on old rows. The flags are pgarray and
|
;; on the table on old rows. The flags are pgarray and
|
||||||
;; for avoid decoding it (because they are no longer used
|
;; for avoid decoding it (because they are no longer used
|
||||||
;; on frontend) we just dissoc the column attribute from
|
;; on frontend) we just dissoc the column attribute from
|
||||||
;; row.
|
;; row.
|
||||||
(dissoc :flags)))))
|
(dissoc :flags)))))
|
||||||
|
(= :share-link (:type perms))
|
||||||
|
(filterv #(= (:id %) share-id)))
|
||||||
|
|
||||||
fonts (db/query conn :team-font-variant
|
fonts (db/query conn :team-font-variant
|
||||||
{:team-id (:id team)
|
{:team-id (:id team)
|
||||||
|
|||||||
@ -1045,3 +1045,18 @@ RETURNING id, deleted_at;")
|
|||||||
(update acc :created conj email)))))
|
(update acc :created conj email)))))
|
||||||
{:created [] :skipped []}
|
{:created [] :skipped []}
|
||||||
emails)))))
|
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.db :as db]
|
||||||
[app.features.fdata :as fdata]
|
[app.features.fdata :as fdata]
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
|
[app.tasks.delete-object :as dobj]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
(def ^:private sql:get-profiles
|
(def ^:private sql:get-profiles
|
||||||
@ -33,6 +34,11 @@
|
|||||||
;; Mark as deleted the storage object
|
;; Mark as deleted the storage object
|
||||||
(some->> photo-id (sto/touch-object! storage))
|
(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})
|
(let [affected (-> (db/delete! conn :profile {:id id})
|
||||||
(db/get-update-count))]
|
(db/get-update-count))]
|
||||||
(+ total affected)))
|
(+ total affected)))
|
||||||
|
|||||||
@ -5,7 +5,12 @@
|
|||||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||||
|
|
||||||
(ns app.util.ssrf
|
(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
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
@ -122,6 +127,20 @@
|
|||||||
;; Check the embedded IPv4 is loopback (127.x.x.x)
|
;; Check the embedded IPv4 is loopback (127.x.x.x)
|
||||||
(= (bit-and (aget bs 12) 0xFF) 127))))
|
(= (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?
|
(defn- blocked-address?
|
||||||
"Check if an InetAddress should be blocked. Returns true if blocked."
|
"Check if an InetAddress should be blocked. Returns true if blocked."
|
||||||
[^InetAddress addr]
|
[^InetAddress addr]
|
||||||
@ -141,12 +160,15 @@
|
|||||||
;; Cloud metadata IPs (exact match)
|
;; Cloud metadata IPs (exact match)
|
||||||
(contains? cloud-metadata-ips (.getHostAddress addr))
|
(contains? cloud-metadata-ips (.getHostAddress addr))
|
||||||
|
|
||||||
;; Extra blocked CIDRs (IPv4 only)
|
;; Extra blocked CIDRs (IPv4 only) and IPv6 transition mechanisms
|
||||||
(let [bs (.getAddress addr)]
|
(let [bs (.getAddress addr)]
|
||||||
(if (= (alength bs) 4)
|
(if (= (alength bs) 4)
|
||||||
(or (some #(in-cidr4? bs %) extra-blocked-ranges)
|
(or (some #(in-cidr4? bs %) extra-blocked-ranges)
|
||||||
(some #(in-cidr4? bs %) extra-blocked-cidrs))
|
(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
|
(defn resolve-host
|
||||||
"Resolve a hostname to all InetAddress objects. Wraps InetAddress/getAllByName
|
"Resolve a hostname to all InetAddress objects. Wraps InetAddress/getAllByName
|
||||||
@ -163,8 +185,10 @@
|
|||||||
- host must resolve to at least one address, and
|
- host must resolve to at least one address, and
|
||||||
- **every** resolved address must NOT be in the blocklist
|
- **every** resolved address must NOT be in the blocklist
|
||||||
(loopback, link-local, site-local, multicast, any-local,
|
(loopback, link-local, site-local, multicast, any-local,
|
||||||
cloud-metadata 169.254.169.254, IPv6 ULA fc00::/7, IPv4-mapped
|
cloud-metadata 169.254.169.254, IPv6 ULA fc00::/7, IPv6 transition
|
||||||
IPv6 of any blocked IPv4, plus operator-supplied CIDRs).
|
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
|
When the host is an IP literal (decimal/octal/hex/IPv6) it is
|
||||||
normalized via `com.google.common.net.InetAddresses` before the
|
normalized via `com.google.common.net.InetAddresses` before the
|
||||||
check.
|
check.
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.storage.tmp :as tmp]
|
[app.storage.tmp :as tmp]
|
||||||
[backend-tests.helpers :as th]
|
[backend-tests.helpers :as th]
|
||||||
|
[backend-tests.storage-test :as stt]
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[datoteka.fs :as fs]
|
[datoteka.fs :as fs]
|
||||||
@ -1883,3 +1884,86 @@
|
|||||||
(t/is (some? ext-libs)
|
(t/is (some? ext-libs)
|
||||||
"type :link-later should produce external-libraries even with include-libraries=true")
|
"type :link-later should produce external-libraries even with include-libraries=true")
|
||||||
(t/is (pos? (count ext-libs))))))
|
(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
|
(ns backend-tests.db-test
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[backend-tests.helpers :as th]
|
[backend-tests.helpers :as th]
|
||||||
[clojure.test :as t])
|
[clojure.test :as t])
|
||||||
@ -41,3 +42,13 @@
|
|||||||
|
|
||||||
(t/testing "maximum pool size is reasonable"
|
(t/testing "maximum pool size is reasonable"
|
||||||
(t/is (pos? (:maximum-pool-size stats))))))
|
(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)})]
|
(let [threads (th/db-query :comment-thread {:file-id (:id file-1)})]
|
||||||
(t/is (= 0 (count threads)))))))))
|
(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
|
(let [out (th/command! {::th/type :create-upload-session
|
||||||
::rpc/profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:total-chunks total-chunks})]
|
:total-chunks total-chunks})]
|
||||||
(t/is (nil? (:error out)))
|
(let [session-id (:session-id (:result out))]
|
||||||
(: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!
|
(defn- upload-font-chunked!
|
||||||
"Splits `font-bytes` into chunks of `chunk-size` bytes, creates an upload
|
"Splits `font-bytes` into chunks of `chunk-size` bytes, creates an upload
|
||||||
@ -68,14 +73,15 @@
|
|||||||
[prof ^bytes font-bytes mtype chunk-size]
|
[prof ^bytes font-bytes mtype chunk-size]
|
||||||
(let [chunks (split-bytes-into-chunks font-bytes chunk-size)
|
(let [chunks (split-bytes-into-chunks font-bytes chunk-size)
|
||||||
session-id (create-upload-session! prof (count chunks))]
|
session-id (create-upload-session! prof (count chunks))]
|
||||||
(doseq [[idx chunk-data] (map-indexed vector chunks)]
|
(when (uuid? session-id)
|
||||||
(let [mfile (make-chunk-mfile chunk-data mtype)
|
(doseq [[idx chunk-data] (map-indexed vector chunks)]
|
||||||
out (th/command! {::th/type :upload-chunk
|
(let [mfile (make-chunk-mfile chunk-data mtype)
|
||||||
::rpc/profile-id (:id prof)
|
out (th/command! {::th/type :upload-chunk
|
||||||
:session-id session-id
|
::rpc/profile-id (:id prof)
|
||||||
:index idx
|
:session-id session-id
|
||||||
:content mfile})]
|
:index idx
|
||||||
(t/is (nil? (:error out)))))
|
:content mfile})]
|
||||||
|
(t/is (nil? (:error out))))))
|
||||||
session-id))
|
session-id))
|
||||||
|
|
||||||
(defn- assert-font-variant-result
|
(defn- assert-font-variant-result
|
||||||
@ -613,43 +619,42 @@
|
|||||||
;; N2-07: A user with edit permissions on their own team must not be
|
;; 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
|
;; able to create a font variant using a font-id that already belongs
|
||||||
;; to another team (BOLA / CWE-639).
|
;; 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})
|
||||||
(let [prof1 (th/create-profile* 1 {:is-active true})
|
prof2 (th/create-profile* 2 {:is-active true})
|
||||||
prof2 (th/create-profile* 2 {:is-active true})
|
team1 (:default-team-id prof1)
|
||||||
team1 (:default-team-id prof1)
|
team2 (:default-team-id prof2)
|
||||||
team2 (:default-team-id prof2)
|
font-id (uuid/custom 10 999)
|
||||||
font-id (uuid/custom 10 999)
|
data (-> (io/resource "backend_tests/test_files/font-1.ttf")
|
||||||
data (-> (io/resource "backend_tests/test_files/font-1.ttf")
|
(io/read*))]
|
||||||
(io/read*))]
|
|
||||||
|
|
||||||
;; prof1 creates a font variant in team1 with font-id
|
;; prof1 creates a font variant in team1 with font-id
|
||||||
(let [session-id (upload-font-chunked! prof1 data "font/ttf" (* 4 1024 1024))
|
(let [session-id (upload-font-chunked! prof1 data "font/ttf" (* 4 1024 1024))
|
||||||
params {::th/type :create-font-variant
|
params {::th/type :create-font-variant
|
||||||
::rpc/profile-id (:id prof1)
|
::rpc/profile-id (:id prof1)
|
||||||
:team-id team1
|
:team-id team1
|
||||||
:font-id font-id
|
:font-id font-id
|
||||||
:font-family "SharedFont"
|
:font-family "SharedFont"
|
||||||
:font-weight 400
|
:font-weight 400
|
||||||
:font-style "normal"
|
:font-style "normal"
|
||||||
:uploads {"font/ttf" session-id}}
|
:uploads {"font/ttf" session-id}}
|
||||||
out (th/command! params)]
|
out (th/command! params)]
|
||||||
(t/is (nil? (:error out))))
|
(t/is (nil? (:error out))))
|
||||||
|
|
||||||
;; prof2 tries to create a variant using the same font-id but
|
;; prof2 tries to create a variant using the same font-id but
|
||||||
;; in team2, which must be rejected because font-id belongs to team1
|
;; 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))
|
(let [session-id (upload-font-chunked! prof2 data "font/ttf" (* 4 1024 1024))
|
||||||
params {::th/type :create-font-variant
|
params {::th/type :create-font-variant
|
||||||
::rpc/profile-id (:id prof2)
|
::rpc/profile-id (:id prof2)
|
||||||
:team-id team2
|
:team-id team2
|
||||||
:font-id font-id
|
:font-id font-id
|
||||||
:font-family "SharedFont"
|
:font-family "SharedFont"
|
||||||
:font-weight 700
|
:font-weight 700
|
||||||
:font-style "normal"
|
:font-style "normal"
|
||||||
:uploads {"font/ttf" session-id}}
|
:uploads {"font/ttf" session-id}}
|
||||||
out (th/command! params)]
|
out (th/command! params)]
|
||||||
(t/is (some? (:error out)))
|
(t/is (some? (:error out)))
|
||||||
(t/is (= :not-found (-> out :error ex-data :type)))
|
(t/is (= :not-found (-> out :error ex-data :type)))
|
||||||
(t/is (= :object-not-found (-> out :error ex-data :code)))))))
|
(t/is (= :object-not-found (-> out :error ex-data :code))))))
|
||||||
|
|
||||||
(t/deftest get-font-variants-nonexistent-file
|
(t/deftest get-font-variants-nonexistent-file
|
||||||
(let [prof (th/create-profile* 1 {:is-active true})
|
(let [prof (th/create-profile* 1 {:is-active true})
|
||||||
|
|||||||
@ -194,6 +194,15 @@
|
|||||||
(string? (get version k)))))
|
(string? (get version k)))))
|
||||||
(t/is (= cf/version version))))
|
(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
|
(t/deftest get-teams-returns-only-owned-non-default-non-deleted
|
||||||
(with-mocks [nitrate-mock {:target 'app.nitrate/call :return nil}]
|
(with-mocks [nitrate-mock {:target 'app.nitrate/call :return nil}]
|
||||||
(let [profile (th/create-profile* 1 {:is-active true})
|
(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)]))]
|
(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/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/deftest email-blacklist-1
|
||||||
(t/is (false? (email.blacklist/enabled? th/*system*)))
|
(t/is (false? (email.blacklist/enabled? th/*system*)))
|
||||||
|
|||||||
@ -1328,3 +1328,83 @@
|
|||||||
out (th/command! data)]
|
out (th/command! data)]
|
||||||
(t/is (th/success? out))
|
(t/is (th/success? out))
|
||||||
(t/is (= 1 (:call-count @mock)))))))
|
(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)]
|
(let [result (:result out)]
|
||||||
(t/is (contains? result :file))
|
(t/is (contains? result :file))
|
||||||
(t/is (contains? result :project)))))))
|
(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://[fd00::1]/foo")))
|
||||||
(t/is (false? (ssrf/safe-url? "http://[fc00::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
|
(t/deftest validate-url-blocks-encoded-loopback
|
||||||
;; Decimal encoding of 127.0.0.1 = 2130706433
|
;; Decimal encoding of 127.0.0.1 = 2130706433
|
||||||
;; InetAddress normalizes this to 127.0.0.1
|
;; InetAddress normalizes this to 127.0.0.1
|
||||||
|
|||||||
@ -17,14 +17,14 @@
|
|||||||
org.slf4j/slf4j-api {:mvn/version "2.0.18"}
|
org.slf4j/slf4j-api {:mvn/version "2.0.18"}
|
||||||
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.43"}
|
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"}
|
criterium/criterium {:mvn/version "0.4.6"}
|
||||||
|
|
||||||
metosin/jsonista {:mvn/version "1.0.0"
|
metosin/jsonista {:mvn/version "1.0.0"
|
||||||
:exclusions [com.fasterxml.jackson.core/jackson-core
|
:exclusions [com.fasterxml.jackson.core/jackson-core
|
||||||
com.fasterxml.jackson.core/jackson-databind]}
|
com.fasterxml.jackson.core/jackson-databind]}
|
||||||
com.fasterxml.jackson.core/jackson-core {: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.1"}
|
com.fasterxml.jackson.core/jackson-databind {:mvn/version "2.22.2"}
|
||||||
|
|
||||||
|
|
||||||
metosin/malli {:mvn/version "0.20.1"}
|
metosin/malli {:mvn/version "0.20.1"}
|
||||||
@ -60,7 +60,7 @@
|
|||||||
{:dev
|
{:dev
|
||||||
{:extra-deps
|
{:extra-deps
|
||||||
{org.clojure/tools.namespace {:mvn/version "1.5.1"}
|
{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.clojure-goes-fast/clj-async-profiler {:mvn/version "2.0.0-beta1"}
|
||||||
com.bhauman/rebel-readline {:mvn/version "0.1.11"}
|
com.bhauman/rebel-readline {:mvn/version "0.1.11"}
|
||||||
criterium/criterium {:mvn/version "0.4.6"}
|
criterium/criterium {:mvn/version "0.4.6"}
|
||||||
|
|||||||
@ -4,18 +4,18 @@
|
|||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"author": "Kaleidos INC Sucursal en España SL",
|
"author": "Kaleidos INC Sucursal en España SL",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/penpot/penpot"
|
"url": "https://github.com/penpot/penpot"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^10.0.4",
|
"concurrently": "^10.0.5",
|
||||||
"nodemon": "^3.1.14",
|
"nodemon": "^3.1.14",
|
||||||
"prettier": "3.9.6",
|
"prettier": "3.9.6",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"ws": "^8.21.2"
|
"ws": "^8.21.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"date-fns": "^4.4.0"
|
"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'
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
@ -13,8 +114,8 @@ importers:
|
|||||||
version: 4.4.0
|
version: 4.4.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
concurrently:
|
concurrently:
|
||||||
specifier: ^10.0.4
|
specifier: ^10.0.5
|
||||||
version: 10.0.4
|
version: 10.0.5
|
||||||
nodemon:
|
nodemon:
|
||||||
specifier: ^3.1.14
|
specifier: ^3.1.14
|
||||||
version: 3.1.14
|
version: 3.1.14
|
||||||
@ -25,8 +126,8 @@ importers:
|
|||||||
specifier: ^0.5.21
|
specifier: ^0.5.21
|
||||||
version: 0.5.21
|
version: 0.5.21
|
||||||
ws:
|
ws:
|
||||||
specifier: ^8.21.2
|
specifier: ^8.21.3
|
||||||
version: 8.21.2
|
version: 8.21.3
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@ -73,8 +174,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
|
resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
concurrently@10.0.4:
|
concurrently@10.0.5:
|
||||||
resolution: {integrity: sha512-trZql+7l/0+WRAsAnEdctr4+iiOS6ZrViI6H8QWcCF9MFS/LT0dKpe8vluB1to6it+OxSI4VospFTIFMW8DJRw==}
|
resolution: {integrity: sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@ -234,8 +335,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
|
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
ws@8.21.2:
|
ws@8.21.3:
|
||||||
resolution: {integrity: sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==}
|
resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
bufferutil: ^4.0.1
|
bufferutil: ^4.0.1
|
||||||
@ -303,7 +404,7 @@ snapshots:
|
|||||||
strip-ansi: 7.2.0
|
strip-ansi: 7.2.0
|
||||||
wrap-ansi: 9.0.2
|
wrap-ansi: 9.0.2
|
||||||
|
|
||||||
concurrently@10.0.4:
|
concurrently@10.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
@ -439,7 +540,7 @@ snapshots:
|
|||||||
string-width: 7.2.0
|
string-width: 7.2.0
|
||||||
strip-ansi: 7.2.0
|
strip-ansi: 7.2.0
|
||||||
|
|
||||||
ws@8.21.2: {}
|
ws@8.21.3: {}
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,7 @@ RUN set -ex; \
|
|||||||
|
|
||||||
FROM base AS setup-node
|
FROM base AS setup-node
|
||||||
|
|
||||||
ENV NODE_VERSION=v24.19.0 \
|
ENV NODE_VERSION=v24.20.0 \
|
||||||
PATH=/opt/node/bin:$PATH
|
PATH=/opt/node/bin:$PATH
|
||||||
|
|
||||||
RUN set -eux; \
|
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>"
|
LABEL maintainer="Penpot <docker@penpot.app>"
|
||||||
|
|
||||||
ENV LANG=en_US.UTF-8 \
|
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>"
|
LABEL maintainer="Penpot <docker@penpot.app>"
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|||||||
@ -3,7 +3,7 @@ LABEL maintainer="Penpot <docker@penpot.app>"
|
|||||||
|
|
||||||
ENV LANG=en_US.UTF-8 \
|
ENV LANG=en_US.UTF-8 \
|
||||||
LC_ALL=en_US.UTF-8 \
|
LC_ALL=en_US.UTF-8 \
|
||||||
NODE_VERSION=v24.18.0 \
|
NODE_VERSION=v24.20.0 \
|
||||||
DEBIAN_FRONTEND=noninteractive \
|
DEBIAN_FRONTEND=noninteractive \
|
||||||
PATH=/opt/node/bin:$PATH
|
PATH=/opt/node/bin:$PATH
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
"@11ty/eleventy-plugin-rss": "^3.0.0",
|
"@11ty/eleventy-plugin-rss": "^3.0.0",
|
||||||
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
|
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
|
||||||
"@tigersway/eleventy-plugin-ancestry": "^1.0.3",
|
"@tigersway/eleventy-plugin-ancestry": "^1.0.3",
|
||||||
"@types/markdown-it": "14.1.2",
|
"@types/markdown-it": "14.2.0",
|
||||||
"elasticlunr": "^0.9.5",
|
"elasticlunr": "^0.9.5",
|
||||||
"eleventy-plugin-metagen": "^1.8.4",
|
"eleventy-plugin-metagen": "^1.8.4",
|
||||||
"eleventy-plugin-nesting-toc": "^1.3.0",
|
"eleventy-plugin-nesting-toc": "^1.3.0",
|
||||||
@ -39,5 +39,5 @@
|
|||||||
"markdown-it-anchor": "^9.2.1",
|
"markdown-it-anchor": "^9.2.1",
|
||||||
"markdown-it-plantuml": "^1.4.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'
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
@ -24,8 +125,8 @@ importers:
|
|||||||
specifier: ^1.0.3
|
specifier: ^1.0.3
|
||||||
version: 1.0.3(@11ty/eleventy@3.1.6)
|
version: 1.0.3(@11ty/eleventy@3.1.6)
|
||||||
'@types/markdown-it':
|
'@types/markdown-it':
|
||||||
specifier: 14.1.2
|
specifier: 14.2.0
|
||||||
version: 14.1.2
|
version: 14.2.0
|
||||||
elasticlunr:
|
elasticlunr:
|
||||||
specifier: ^0.9.5
|
specifier: ^0.9.5
|
||||||
version: 0.9.5
|
version: 0.9.5
|
||||||
@ -46,7 +147,7 @@ importers:
|
|||||||
version: 15.0.0
|
version: 15.0.0
|
||||||
markdown-it-anchor:
|
markdown-it-anchor:
|
||||||
specifier: ^9.2.1
|
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:
|
markdown-it-plantuml:
|
||||||
specifier: ^1.4.1
|
specifier: ^1.4.1
|
||||||
version: 1.4.1
|
version: 1.4.1
|
||||||
@ -64,8 +165,8 @@ packages:
|
|||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@11ty/eleventy-fetch@5.1.2':
|
'@11ty/eleventy-fetch@5.1.3':
|
||||||
resolution: {integrity: sha512-YxDARdR3S9UT4gOGRWgGNyokYT9jkCAjJge3OVKFdNMv1cyvWg1NHCvj9NVvK9XHenCLFy3cwvM/YYpZZTZopw==}
|
resolution: {integrity: sha512-4HS6QB/mVWTVlE6kjCKPkjkC1Z0YOqizJaHR05zK+E7a2b4EUC3T+wLktIy7wEl76ZoarkY45EKLmTw1XuR8fQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@11ty/eleventy-navigation@1.0.5':
|
'@11ty/eleventy-navigation@1.0.5':
|
||||||
@ -102,8 +203,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-oI7m8pa7/IAU/3lqRU9vjBbs20iKFo7x+1K9kT3aVira6scc1X9MjBdgLCHzLJeJ7iB6wydioA+kr9/qPnvmlQ==}
|
resolution: {integrity: sha512-oI7m8pa7/IAU/3lqRU9vjBbs20iKFo7x+1K9kT3aVira6scc1X9MjBdgLCHzLJeJ7iB6wydioA+kr9/qPnvmlQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@rgrove/parse-xml@4.2.0':
|
'@rgrove/parse-xml@4.2.3':
|
||||||
resolution: {integrity: sha512-UuBOt7BOsKVOkFXRe4Ypd/lADuNIfqJXv8GvHqtXaTYXPPKkj2nS2zPllVsrtRjcomDhIJVBnZwfmlI222WH8g==}
|
resolution: {integrity: sha512-Jhlb+0zYez1T1yXUQs3F1qAtFuJljBVNdy9TKmLDauAXkxsOXopKYOyQ5Wm6SvP3fycav0GviX4Y15WWhGetMw==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
'@sindresorhus/slugify@2.2.1':
|
'@sindresorhus/slugify@2.2.1':
|
||||||
@ -122,8 +223,8 @@ packages:
|
|||||||
'@types/linkify-it@5.0.0':
|
'@types/linkify-it@5.0.0':
|
||||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||||
|
|
||||||
'@types/markdown-it@14.1.2':
|
'@types/markdown-it@14.2.0':
|
||||||
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
|
resolution: {integrity: sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==}
|
||||||
|
|
||||||
'@types/mdurl@2.0.0':
|
'@types/mdurl@2.0.0':
|
||||||
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
|
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
|
||||||
@ -135,8 +236,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==}
|
resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
acorn@8.17.0:
|
acorn@8.18.0:
|
||||||
resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
|
resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@ -150,8 +251,8 @@ packages:
|
|||||||
argparse@2.0.1:
|
argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
argparse@3.0.0:
|
argparse@3.0.1:
|
||||||
resolution: {integrity: sha512-BOp5NMrHqKxmq/OLr+clzzrRxgOKSLkcjmkWuChp7Irqwn4s74WjOBPIgWfA/HMcBnVkZ5XEuf9uUqzlpfCQ6A==}
|
resolution: {integrity: sha512-nM4mHF/KM1v59ZNKX7zfusQz5wUAxR511YG8Vo6TyiV4aqhu++rbJW4v04xsWhpSsHFj66flT8P7znVpyO20xQ==}
|
||||||
|
|
||||||
asap@2.0.6:
|
asap@2.0.6:
|
||||||
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
||||||
@ -165,8 +266,8 @@ packages:
|
|||||||
bcp-47-normalize@2.3.0:
|
bcp-47-normalize@2.3.0:
|
||||||
resolution: {integrity: sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==}
|
resolution: {integrity: sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==}
|
||||||
|
|
||||||
bcp-47@2.1.0:
|
bcp-47@2.1.1:
|
||||||
resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==}
|
resolution: {integrity: sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==}
|
||||||
|
|
||||||
binary-extensions@2.3.0:
|
binary-extensions@2.3.0:
|
||||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||||
@ -363,8 +464,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
|
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
flatted@3.4.2:
|
flatted@3.4.4:
|
||||||
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
|
resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
|
||||||
|
|
||||||
fresh@2.0.0:
|
fresh@2.0.0:
|
||||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||||
@ -439,12 +540,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||||
engines: {node: '>=0.12.0'}
|
engines: {node: '>=0.12.0'}
|
||||||
|
|
||||||
iso-639-1@3.1.5:
|
iso-639-1@3.1.6:
|
||||||
resolution: {integrity: sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA==}
|
resolution: {integrity: sha512-ZFar/L4ngX7wZh2QX+Fiftmuf0igWJsrJtfizrovWifF1gAWkfmRa5Z1m0LQZbm0hKCHRDYhLRSLFrSqNe4EJA==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
|
|
||||||
js-yaml@3.15.0:
|
js-yaml@3.15.1:
|
||||||
resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==}
|
resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
js-yaml@4.3.1:
|
js-yaml@4.3.1:
|
||||||
@ -469,8 +570,8 @@ packages:
|
|||||||
linkify-it@6.1.0:
|
linkify-it@6.1.0:
|
||||||
resolution: {integrity: sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==}
|
resolution: {integrity: sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==}
|
||||||
|
|
||||||
liquidjs@10.28.0:
|
liquidjs@10.29.0:
|
||||||
resolution: {integrity: sha512-b6tmBXYMQTuGPnM5vB0CuZMo5kvmKMtSB/gvUWP6RFn2pIB5s+bJkBfIyJnattkc5bgeAiflrZVptx3iaLLioQ==}
|
resolution: {integrity: sha512-pCVOhs6FLAR8su3ItJ07diN26t6W5dHQRnmTMy8HPyTFuv1+oSCVJIGp5pGjfQyOZfh50KswvKtMTp6p4JEIdw==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@ -598,8 +699,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
|
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
picomatch@4.0.4:
|
picomatch@4.0.7:
|
||||||
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
please-upgrade-node@3.2.0:
|
please-upgrade-node@3.2.0:
|
||||||
@ -634,8 +735,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
range-parser@1.2.1:
|
range-parser@1.3.0:
|
||||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
readdirp@3.6.0:
|
readdirp@3.6.0:
|
||||||
@ -652,8 +753,8 @@ packages:
|
|||||||
semver-compare@1.0.0:
|
semver-compare@1.0.0:
|
||||||
resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==}
|
resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==}
|
||||||
|
|
||||||
semver@7.8.4:
|
semver@7.8.5:
|
||||||
resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==}
|
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@ -729,8 +830,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
ws@8.21.0:
|
ws@8.21.3:
|
||||||
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
|
resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
bufferutil: ^4.0.1
|
bufferutil: ^4.0.1
|
||||||
@ -746,7 +847,7 @@ snapshots:
|
|||||||
'@11ty/dependency-tree-esm@2.0.4':
|
'@11ty/dependency-tree-esm@2.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@11ty/eleventy-utils': 2.0.7
|
'@11ty/eleventy-utils': 2.0.7
|
||||||
acorn: 8.17.0
|
acorn: 8.18.0
|
||||||
dependency-graph: 1.0.0
|
dependency-graph: 1.0.0
|
||||||
normalize-path: 3.0.0
|
normalize-path: 3.0.0
|
||||||
|
|
||||||
@ -767,18 +868,18 @@ snapshots:
|
|||||||
send: 1.2.1
|
send: 1.2.1
|
||||||
ssri: 11.0.0
|
ssri: 11.0.0
|
||||||
urlpattern-polyfill: 10.1.0
|
urlpattern-polyfill: 10.1.0
|
||||||
ws: 8.21.0
|
ws: 8.21.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
'@11ty/eleventy-fetch@5.1.2':
|
'@11ty/eleventy-fetch@5.1.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@11ty/eleventy-utils': 2.0.7
|
'@11ty/eleventy-utils': 2.0.7
|
||||||
'@rgrove/parse-xml': 4.2.0
|
'@rgrove/parse-xml': 4.2.3
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
flatted: 3.4.2
|
flatted: 3.4.4
|
||||||
p-queue: 6.6.2
|
p-queue: 6.6.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@ -829,21 +930,21 @@ snapshots:
|
|||||||
entities: 6.0.1
|
entities: 6.0.1
|
||||||
filesize: 10.1.6
|
filesize: 10.1.6
|
||||||
gray-matter: 4.0.3
|
gray-matter: 4.0.3
|
||||||
iso-639-1: 3.1.5
|
iso-639-1: 3.1.6
|
||||||
js-yaml: 4.3.1
|
js-yaml: 4.3.1
|
||||||
kleur: 4.1.5
|
kleur: 4.1.5
|
||||||
liquidjs: 10.28.0
|
liquidjs: 10.29.0
|
||||||
luxon: 3.7.2
|
luxon: 3.7.2
|
||||||
markdown-it: 14.3.0
|
markdown-it: 14.3.0
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
moo: 0.5.2
|
moo: 0.5.2
|
||||||
node-retrieve-globals: 6.0.1
|
node-retrieve-globals: 6.0.1
|
||||||
nunjucks: 3.2.4(chokidar@3.6.0)
|
nunjucks: 3.2.4(chokidar@3.6.0)
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.7
|
||||||
please-upgrade-node: 3.2.0
|
please-upgrade-node: 3.2.0
|
||||||
posthtml: 0.16.7
|
posthtml: 0.16.7
|
||||||
posthtml-match-helper: 2.0.3(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
|
slugify: 1.6.9
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -867,7 +968,7 @@ snapshots:
|
|||||||
minimatch: 3.1.5
|
minimatch: 3.1.5
|
||||||
slash: 3.0.0
|
slash: 3.0.0
|
||||||
|
|
||||||
'@rgrove/parse-xml@4.2.0': {}
|
'@rgrove/parse-xml@4.2.3': {}
|
||||||
|
|
||||||
'@sindresorhus/slugify@2.2.1':
|
'@sindresorhus/slugify@2.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -884,7 +985,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/linkify-it@5.0.0': {}
|
'@types/linkify-it@5.0.0': {}
|
||||||
|
|
||||||
'@types/markdown-it@14.1.2':
|
'@types/markdown-it@14.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/linkify-it': 5.0.0
|
'@types/linkify-it': 5.0.0
|
||||||
'@types/mdurl': 2.0.0
|
'@types/mdurl': 2.0.0
|
||||||
@ -895,9 +996,9 @@ snapshots:
|
|||||||
|
|
||||||
acorn-walk@8.3.5:
|
acorn-walk@8.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.17.0
|
acorn: 8.18.0
|
||||||
|
|
||||||
acorn@8.17.0: {}
|
acorn@8.18.0: {}
|
||||||
|
|
||||||
anymatch@3.1.3:
|
anymatch@3.1.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -910,7 +1011,7 @@ snapshots:
|
|||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
argparse@3.0.0: {}
|
argparse@3.0.1: {}
|
||||||
|
|
||||||
asap@2.0.6: {}
|
asap@2.0.6: {}
|
||||||
|
|
||||||
@ -920,10 +1021,10 @@ snapshots:
|
|||||||
|
|
||||||
bcp-47-normalize@2.3.0:
|
bcp-47-normalize@2.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
bcp-47: 2.1.0
|
bcp-47: 2.1.1
|
||||||
bcp-47-match: 2.0.3
|
bcp-47-match: 2.0.3
|
||||||
|
|
||||||
bcp-47@2.1.0:
|
bcp-47@2.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-alphabetical: 2.0.1
|
is-alphabetical: 2.0.1
|
||||||
is-alphanumerical: 2.0.1
|
is-alphanumerical: 2.0.1
|
||||||
@ -1060,7 +1161,7 @@ snapshots:
|
|||||||
|
|
||||||
eleventy-plugin-youtube-embed@1.13.2:
|
eleventy-plugin-youtube-embed@1.13.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@11ty/eleventy-fetch': 5.1.2
|
'@11ty/eleventy-fetch': 5.1.3
|
||||||
deepmerge: 4.3.1
|
deepmerge: 4.3.1
|
||||||
lite-youtube-embed: 0.3.4
|
lite-youtube-embed: 0.3.4
|
||||||
string-replace-async: 3.0.2
|
string-replace-async: 3.0.2
|
||||||
@ -1096,7 +1197,7 @@ snapshots:
|
|||||||
|
|
||||||
esm-import-transformer@3.0.5:
|
esm-import-transformer@3.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.17.0
|
acorn: 8.18.0
|
||||||
|
|
||||||
esprima@4.0.1: {}
|
esprima@4.0.1: {}
|
||||||
|
|
||||||
@ -1110,9 +1211,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-extendable: 0.1.1
|
is-extendable: 0.1.1
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.4):
|
fdir@6.5.0(picomatch@4.0.7):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.7
|
||||||
|
|
||||||
filesize@10.1.6: {}
|
filesize@10.1.6: {}
|
||||||
|
|
||||||
@ -1132,7 +1233,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
flatted@3.4.2: {}
|
flatted@3.4.4: {}
|
||||||
|
|
||||||
fresh@2.0.0: {}
|
fresh@2.0.0: {}
|
||||||
|
|
||||||
@ -1147,7 +1248,7 @@ snapshots:
|
|||||||
|
|
||||||
gray-matter@4.0.3:
|
gray-matter@4.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
js-yaml: 3.15.0
|
js-yaml: 3.15.1
|
||||||
kind-of: 6.0.3
|
kind-of: 6.0.3
|
||||||
section-matter: 1.0.0
|
section-matter: 1.0.0
|
||||||
strip-bom-string: 1.0.0
|
strip-bom-string: 1.0.0
|
||||||
@ -1207,9 +1308,9 @@ snapshots:
|
|||||||
|
|
||||||
is-number@7.0.0: {}
|
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:
|
dependencies:
|
||||||
argparse: 1.0.10
|
argparse: 1.0.10
|
||||||
esprima: 4.0.1
|
esprima: 4.0.1
|
||||||
@ -1232,7 +1333,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
uc.micro: 3.0.0
|
uc.micro: 3.0.0
|
||||||
|
|
||||||
liquidjs@10.28.0:
|
liquidjs@10.29.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
commander: 10.0.1
|
commander: 10.0.1
|
||||||
|
|
||||||
@ -1242,9 +1343,9 @@ snapshots:
|
|||||||
|
|
||||||
luxon@3.7.2: {}
|
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:
|
dependencies:
|
||||||
'@types/markdown-it': 14.1.2
|
'@types/markdown-it': 14.2.0
|
||||||
markdown-it: 15.0.0
|
markdown-it: 15.0.0
|
||||||
|
|
||||||
markdown-it-plantuml@1.4.1: {}
|
markdown-it-plantuml@1.4.1: {}
|
||||||
@ -1260,7 +1361,7 @@ snapshots:
|
|||||||
|
|
||||||
markdown-it@15.0.0:
|
markdown-it@15.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
argparse: 3.0.0
|
argparse: 3.0.1
|
||||||
entities: 8.0.0
|
entities: 8.0.0
|
||||||
linkify-it: 6.1.0
|
linkify-it: 6.1.0
|
||||||
mdurl: 2.1.0
|
mdurl: 2.1.0
|
||||||
@ -1299,7 +1400,7 @@ snapshots:
|
|||||||
|
|
||||||
node-retrieve-globals@6.0.1:
|
node-retrieve-globals@6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.17.0
|
acorn: 8.18.0
|
||||||
acorn-walk: 8.3.5
|
acorn-walk: 8.3.5
|
||||||
esm-import-transformer: 3.0.5
|
esm-import-transformer: 3.0.5
|
||||||
|
|
||||||
@ -1351,7 +1452,7 @@ snapshots:
|
|||||||
|
|
||||||
picomatch@2.3.2: {}
|
picomatch@2.3.2: {}
|
||||||
|
|
||||||
picomatch@4.0.4: {}
|
picomatch@4.0.7: {}
|
||||||
|
|
||||||
please-upgrade-node@3.2.0:
|
please-upgrade-node@3.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1380,7 +1481,7 @@ snapshots:
|
|||||||
|
|
||||||
punycode.js@2.3.1: {}
|
punycode.js@2.3.1: {}
|
||||||
|
|
||||||
range-parser@1.2.1: {}
|
range-parser@1.3.0: {}
|
||||||
|
|
||||||
readdirp@3.6.0:
|
readdirp@3.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1395,7 +1496,7 @@ snapshots:
|
|||||||
|
|
||||||
semver-compare@1.0.0: {}
|
semver-compare@1.0.0: {}
|
||||||
|
|
||||||
semver@7.8.4: {}
|
semver@7.8.5: {}
|
||||||
|
|
||||||
send@1.2.1:
|
send@1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1408,7 +1509,7 @@ snapshots:
|
|||||||
mime-types: 3.0.2
|
mime-types: 3.0.2
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
on-finished: 2.4.1
|
on-finished: 2.4.1
|
||||||
range-parser: 1.2.1
|
range-parser: 1.3.0
|
||||||
statuses: 2.0.2
|
statuses: 2.0.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@ -1433,8 +1534,8 @@ snapshots:
|
|||||||
|
|
||||||
tinyglobby@0.2.17:
|
tinyglobby@0.2.17:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0(picomatch@4.0.4)
|
fdir: 6.5.0(picomatch@4.0.7)
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.7
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1458,4 +1559,4 @@ snapshots:
|
|||||||
|
|
||||||
whatwg-mimetype@4.0.0: {}
|
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
|
```bash
|
||||||
./manage.sh run-devenv --agentic \
|
./manage.sh run-devenv --agentic \
|
||||||
[--ws N] [--sync] [--serena-context CTX] \
|
[--ws N] [--sync] [--serena-context CTX] \
|
||||||
|
[--opencode-config-dir DIR] \
|
||||||
[--git-user-name NAME] [--git-user-email EMAIL]
|
[--git-user-name NAME] [--git-user-email EMAIL]
|
||||||
```
|
```
|
||||||
|
|
||||||
Brings one agentic instance up. Errors out if the target is already running.
|
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
|
`--ws N` (N ≥ 1) brings that workspace up independently — workspaces can be
|
||||||
started and stopped in any order. Per-instance ports
|
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
|
`run-devenv` brings an instance up (idempotent), so re-running
|
||||||
with different flags is the way to change the in-container identity.
|
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
|
### Shared state and workers
|
||||||
|
|
||||||
All instances share one Penpot database and one MinIO bucket; users, teams,
|
All instances share one Penpot database and one MinIO bucket; users, teams,
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
:dev
|
:dev
|
||||||
{:extra-deps
|
{:extra-deps
|
||||||
{thheller/shadow-cljs {:mvn/version "3.4.11"}}}
|
{thheller/shadow-cljs {:mvn/version "3.5.0"}}}
|
||||||
|
|
||||||
:shadow-cljs
|
:shadow-cljs
|
||||||
{:main-opts ["-m" "shadow.cljs.devtools.cli"]
|
{:main-opts ["-m" "shadow.cljs.devtools.cli"]
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"author": "Kaleidos INC Sucursal en España SL",
|
"author": "Kaleidos INC Sucursal en España SL",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/penpot/penpot"
|
"url": "https://github.com/penpot/penpot"
|
||||||
@ -21,12 +21,12 @@
|
|||||||
"playwright": "1.62.1",
|
"playwright": "1.62.1",
|
||||||
"raw-body": "^4.0.0",
|
"raw-body": "^4.0.0",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"undici": "^8.9.0",
|
"undici": "^8.10.0",
|
||||||
"xml-js": "^1.6.11",
|
"xml-js": "^1.6.11",
|
||||||
"xregexp": "^5.1.2"
|
"xregexp": "^5.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ws": "^8.21.1"
|
"ws": "^8.21.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clear:shadow-cache": "rm -rf .shadow-cljs && rm -rf target",
|
"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'
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
@ -45,8 +146,8 @@ importers:
|
|||||||
specifier: ^0.5.21
|
specifier: ^0.5.21
|
||||||
version: 0.5.21
|
version: 0.5.21
|
||||||
undici:
|
undici:
|
||||||
specifier: ^8.9.0
|
specifier: ^8.10.0
|
||||||
version: 8.9.0
|
version: 8.10.0
|
||||||
xml-js:
|
xml-js:
|
||||||
specifier: ^1.6.11
|
specifier: ^1.6.11
|
||||||
version: 1.6.11
|
version: 1.6.11
|
||||||
@ -55,8 +156,8 @@ importers:
|
|||||||
version: 5.1.2
|
version: 5.1.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
ws:
|
ws:
|
||||||
specifier: ^8.21.1
|
specifier: ^8.21.3
|
||||||
version: 8.21.1
|
version: 8.21.3
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@ -420,15 +521,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
|
resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
|
||||||
engines: {node: '>=0.6.x'}
|
engines: {node: '>=0.6.x'}
|
||||||
|
|
||||||
undici@8.9.0:
|
undici@8.10.0:
|
||||||
resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==}
|
resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==}
|
||||||
engines: {node: '>=22.19.0'}
|
engines: {node: '>=22.19.0'}
|
||||||
|
|
||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
ws@8.21.1:
|
ws@8.21.3:
|
||||||
resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==}
|
resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
bufferutil: ^4.0.1
|
bufferutil: ^4.0.1
|
||||||
@ -802,11 +903,11 @@ snapshots:
|
|||||||
|
|
||||||
tsscmp@1.0.6: {}
|
tsscmp@1.0.6: {}
|
||||||
|
|
||||||
undici@8.9.0: {}
|
undici@8.10.0: {}
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
ws@8.21.1: {}
|
ws@8.21.3: {}
|
||||||
|
|
||||||
xml-js@1.6.11:
|
xml-js@1.6.11:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
penpot/common
|
penpot/common
|
||||||
{:local/root "../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"}
|
binaryage/devtools {:mvn/version "RELEASE"}
|
||||||
metosin/reitit-core {:mvn/version "0.10.1"}
|
metosin/reitit-core {:mvn/version "0.10.1"}
|
||||||
funcool/okulary {:mvn/version "2022.04.11-16"}
|
funcool/okulary {:mvn/version "2022.04.11-16"}
|
||||||
@ -51,7 +51,7 @@
|
|||||||
"--enable-native-access=ALL-UNNAMED"]
|
"--enable-native-access=ALL-UNNAMED"]
|
||||||
|
|
||||||
:extra-deps
|
: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"}
|
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
|
||||||
org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
||||||
criterium/criterium {:mvn/version "0.4.6"}}}
|
criterium/criterium {:mvn/version "0.4.6"}}}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"author": "Kaleidos INC Sucursal en España SL",
|
"author": "Kaleidos INC Sucursal en España SL",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"defaults"
|
"defaults"
|
||||||
],
|
],
|
||||||
@ -59,33 +59,33 @@
|
|||||||
"@penpot/tokenscript": "link:packages/tokenscript",
|
"@penpot/tokenscript": "link:packages/tokenscript",
|
||||||
"@penpot/ua-parser": "penpot/ua-parser#1.0.0",
|
"@penpot/ua-parser": "penpot/ua-parser#1.0.0",
|
||||||
"@playwright/test": "1.62.1",
|
"@playwright/test": "1.62.1",
|
||||||
"@storybook/addon-docs": "10.5.6",
|
"@storybook/addon-docs": "10.5.10",
|
||||||
"@storybook/addon-themes": "10.5.6",
|
"@storybook/addon-themes": "10.5.10",
|
||||||
"@storybook/addon-vitest": "10.5.6",
|
"@storybook/addon-vitest": "10.5.10",
|
||||||
"@storybook/react-vite": "10.5.6",
|
"@storybook/react-vite": "10.5.10",
|
||||||
"@tokens-studio/sd-transforms": "2.0.3",
|
"@tokens-studio/sd-transforms": "2.0.3",
|
||||||
"@types/node": "^26.1.2",
|
"@types/node": "^26.4.0",
|
||||||
"@vitest/browser": "4.1.10",
|
"@vitest/browser": "4.1.11",
|
||||||
"@vitest/browser-playwright": "4.1.10",
|
"@vitest/browser-playwright": "4.1.11",
|
||||||
"@vitest/coverage-v8": "4.1.10",
|
"@vitest/coverage-v8": "4.1.11",
|
||||||
"@zip.js/zip.js": "2.8.34",
|
"@zip.js/zip.js": "2.8.34",
|
||||||
"autoprefixer": "^10.5.4",
|
"autoprefixer": "^10.5.4",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
"concurrently": "^10.0.4",
|
"concurrently": "^10.0.5",
|
||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.4.0",
|
||||||
"esbuild": "^0.28.1",
|
"esbuild": "^0.28.2",
|
||||||
"eventsource-parser": "^3.1.0",
|
"eventsource-parser": "^4.1.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"fancy-log": "^2.0.0",
|
"fancy-log": "^2.0.0",
|
||||||
"getopts": "^2.3.0",
|
"getopts": "^2.3.0",
|
||||||
"gettext-parser": "^9.1.1",
|
"gettext-parser": "^9.1.1",
|
||||||
"highlight.js": "^11.10.0",
|
"highlight.js": "^11.12.0",
|
||||||
"js-beautify": "^2.0.3",
|
"js-beautify": "^2.0.3",
|
||||||
"jsdom": "^30.0.1",
|
"jsdom": "^30.0.1",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.18.1",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"map-stream": "0.0.7",
|
"map-stream": "0.0.7",
|
||||||
"marked": "^18.0.9",
|
"marked": "^18.0.11",
|
||||||
"mkdirp": "^3.0.1",
|
"mkdirp": "^3.0.1",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"nodemon": "^3.1.14",
|
"nodemon": "^3.1.14",
|
||||||
@ -93,7 +93,7 @@
|
|||||||
"opentype.js": "^2.0.0",
|
"opentype.js": "^2.0.0",
|
||||||
"p-limit": "^7.3.1",
|
"p-limit": "^7.3.1",
|
||||||
"playwright": "1.62.1",
|
"playwright": "1.62.1",
|
||||||
"postcss": "^8.5.25",
|
"postcss": "^8.5.26",
|
||||||
"postcss-clean": "^1.2.2",
|
"postcss-clean": "^1.2.2",
|
||||||
"postcss-modules": "^9.0.1",
|
"postcss-modules": "^9.0.1",
|
||||||
"postcss-scss": "^4.0.9",
|
"postcss-scss": "^4.0.9",
|
||||||
@ -103,27 +103,27 @@
|
|||||||
"randomcolor": "^0.6.2",
|
"randomcolor": "^0.6.2",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "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",
|
"react-virtualized": "^9.22.6",
|
||||||
"rimraf": "^6.1.3",
|
"rimraf": "^6.1.3",
|
||||||
"rxjs": "8.0.0-alpha.14",
|
"rxjs": "8.0.0-alpha.14",
|
||||||
"sass": "^1.102.0",
|
"sass": "^1.103.1",
|
||||||
"sass-embedded": "^1.100.0",
|
"sass-embedded": "^1.103.1",
|
||||||
"sax": "^1.6.1",
|
"sax": "^1.6.1",
|
||||||
"scheduler": "^0.27.0",
|
"scheduler": "^0.27.0",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"storybook": "10.5.6",
|
"storybook": "10.5.10",
|
||||||
"style-dictionary": "5.5.0",
|
"style-dictionary": "5.5.2",
|
||||||
"stylelint": "^17.14.1",
|
"stylelint": "^17.14.1",
|
||||||
"stylelint-config-standard-scss": "^17.0.0",
|
"stylelint-config-standard-scss": "^17.0.0",
|
||||||
"stylelint-plugin-logical-css": "^2.1.0",
|
"stylelint-plugin-logical-css": "^2.1.0",
|
||||||
"stylelint-scss": "^7.2.0",
|
"stylelint-scss": "^7.2.0",
|
||||||
"svg-sprite": "^2.0.4",
|
"svg-sprite": "^2.0.4",
|
||||||
"tdigest": "^0.1.2",
|
"tdigest": "^0.1.3",
|
||||||
"tinycolor2": "^1.6.0",
|
"tinycolor2": "^1.6.0",
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.2",
|
||||||
"vite": "^8.2.0",
|
"vite": "^8.2.2",
|
||||||
"vitest": "^4.1.10",
|
"vitest": "^4.1.11",
|
||||||
"wait-on": "^9.1.0",
|
"wait-on": "^9.1.0",
|
||||||
"watcher": "^2.3.1",
|
"watcher": "^2.3.1",
|
||||||
"workerpool": "^10.0.3",
|
"workerpool": "^10.0.3",
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
"description": "Penpot Draft-JS Wrapper",
|
"description": "Penpot Draft-JS Wrapper",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||||
"author": "Andrey Antukh",
|
"author": "Andrey Antukh",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
"description": "Simple library for handling keyboard shortcuts",
|
"description": "Simple library for handling keyboard shortcuts",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||||
"author": "Craig Campbell",
|
"author": "Craig Campbell",
|
||||||
"license": "Apache-2.0 WITH LLVM-exception"
|
"license": "Apache-2.0 WITH LLVM-exception"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||||
"author": "Andrey Antukh",
|
"author": "Andrey Antukh",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -20,20 +20,20 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^8.0.1",
|
"@babel/core": "^8.0.1",
|
||||||
"@babel/preset-react": "^8.0.1",
|
"@babel/preset-react": "^8.0.1",
|
||||||
"@storybook/react": "10.5.6",
|
"@storybook/react": "10.5.10",
|
||||||
"@storybook/react-vite": "10.5.6",
|
"@storybook/react-vite": "10.5.10",
|
||||||
"@testing-library/dom": "10.4.1",
|
"@testing-library/dom": "10.4.1",
|
||||||
"@testing-library/react": "16.3.2",
|
"@testing-library/react": "16.3.2",
|
||||||
"@types/react": "^19.2.18",
|
"@types/react": "^19.2.18",
|
||||||
"@types/react-dom": "^19.2.4",
|
"@types/react-dom": "^19.2.5",
|
||||||
"@vitejs/plugin-react": "^6.0.5",
|
"@vitejs/plugin-react": "^6.1.0",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"eslint-plugin-import": "2.32.0",
|
"eslint-plugin-import": "2.32.0",
|
||||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||||
"eslint-plugin-react": "7.37.5",
|
"eslint-plugin-react": "7.37.5",
|
||||||
"eslint-plugin-react-hooks": "7.1.1",
|
"eslint-plugin-react-hooks": "7.1.1",
|
||||||
"react-compiler-runtime": "^1.0.0",
|
"react-compiler-runtime": "^1.0.0",
|
||||||
"storybook": "10.5.6",
|
"storybook": "10.5.10",
|
||||||
"vite-plugin-dts": "^5.0.3"
|
"vite-plugin-dts": "^5.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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.login :refer [login-page*]]
|
||||||
[app.main.ui.auth.recovery :refer [recovery-page*]]
|
[app.main.ui.auth.recovery :refer [recovery-page*]]
|
||||||
[app.main.ui.auth.recovery-request :refer [recovery-request-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.assets.raw-svg :refer [raw-svg*] :as raw-svg]
|
||||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
@ -74,7 +74,7 @@
|
|||||||
[:> recovery-page* {:params params}])
|
[:> recovery-page* {:params params}])
|
||||||
|
|
||||||
(when (= section :auth-register)
|
(when (= section :auth-register)
|
||||||
[:> terms-register*])]]))
|
[:> terms-service-privacy-policy*])]]))
|
||||||
|
|
||||||
|
|
||||||
(mf/defc auth-page*
|
(mf/defc auth-page*
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
@use "ds/_sizes.scss" as *;
|
@use "ds/_sizes.scss" as *;
|
||||||
@use "ds/_utils.scss" as *;
|
@use "ds/_utils.scss" as *;
|
||||||
|
@use "./ds/mixins.scss" as *;
|
||||||
|
|
||||||
.auth-section {
|
.auth-section {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -26,6 +27,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.auth-section.register {
|
.auth-section.register {
|
||||||
|
@include custom-scrollbar;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -4,56 +4,15 @@
|
|||||||
//
|
//
|
||||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||||
|
|
||||||
@use "ds/_utils.scss" as *;
|
|
||||||
@use "ds/_sizes.scss" as *;
|
@use "ds/_sizes.scss" as *;
|
||||||
@use "ds/_borders.scss" as *;
|
@use "ds/_borders.scss" as *;
|
||||||
@use "ds/typography.scss" as *;
|
@use "ds/typography.scss" as *;
|
||||||
|
|
||||||
.auth-form-wrapper {
|
.form {
|
||||||
inline-size: 100%;
|
display: flex;
|
||||||
padding-block-end: 0;
|
flex-direction: column;
|
||||||
display: grid;
|
|
||||||
gap: var(--sp-m);
|
gap: var(--sp-m);
|
||||||
|
margin: var(--sp-m) 0;
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field {
|
.form-field {
|
||||||
@ -62,36 +21,40 @@
|
|||||||
--input-min-width: 100%;
|
--input-min-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons-stack {
|
.form-submit-btn {
|
||||||
display: grid;
|
--button-bg-color: var(--color-accent-primary);
|
||||||
gap: var(--sp-s);
|
--button-border-color: var(--color-accent-primary);
|
||||||
}
|
--button-fg-color: var(--color-background-secondary);
|
||||||
|
|
||||||
.login-button,
|
|
||||||
.login-ldap-button {
|
|
||||||
@include use-typography("headline-small");
|
@include use-typography("headline-small");
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
background-color: var(--button-bg-color);
|
||||||
background-color: var(--color-accent-primary);
|
border: $b-1 solid var(--button-border-color);
|
||||||
border: $b-1 solid var(--color-accent-primary);
|
color: var(--button-fg-color);
|
||||||
color: var(--color-background-secondary);
|
|
||||||
border-radius: $br-8;
|
border-radius: $br-8;
|
||||||
min-block-size: $sz-32;
|
min-block-size: $sz-32;
|
||||||
block-size: $sz-40;
|
block-size: $sz-40;
|
||||||
inline-size: 100%;
|
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 {
|
&:disabled {
|
||||||
background-color: var(--color-background-quaternary);
|
--button-bg-color: var(--color-background-quaternary);
|
||||||
border: 1px solid var(--color-background-quaternary);
|
--button-border-color: var(--color-background-quaternary);
|
||||||
color: var(--color-foreground-disabled);
|
--button-fg-color: var(--color-foreground-disabled);
|
||||||
cursor: unset;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.go-back {
|
.go-back-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--sp-m);
|
gap: var(--sp-m);
|
||||||
@ -100,19 +63,68 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.go-back-link {
|
.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;
|
background: none;
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: $br-8;
|
border-radius: $br-8;
|
||||||
background-color: var(--color-background-tertiary);
|
background-color: var(--button-bg-color);
|
||||||
border: $b-1 solid var(--color-background-tertiary);
|
border: $b-1 solid var(--button-border-color);
|
||||||
color: var(--color-foreground-secondary);
|
color: var(--button-fg-color);
|
||||||
|
|
||||||
@include use-typography("headline-small");
|
|
||||||
|
|
||||||
block-size: $sz-40;
|
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 {
|
.links {
|
||||||
@ -120,104 +132,10 @@
|
|||||||
gap: var(--sp-xxl);
|
gap: var(--sp-xxl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.register,
|
.notification-email {
|
||||||
.account,
|
@include use-typography("title-medium");
|
||||||
.recovery-request,
|
|
||||||
.demo-account {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--sp-s);
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.register-text,
|
line-height: 1.2;
|
||||||
.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;
|
|
||||||
color: var(--color-accent-primary);
|
color: var(--color-accent-primary);
|
||||||
|
margin-inline: $sz-36;
|
||||||
&: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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,10 +71,17 @@
|
|||||||
(mf/defc login-form*
|
(mf/defc login-form*
|
||||||
[{:keys [params handle-redirect on-success-callback on-recovery-request origin] :as props}]
|
[{:keys [params handle-redirect on-success-callback on-recovery-request origin] :as props}]
|
||||||
(let [initial (mf/with-memo [params] params)
|
(let [initial (mf/with-memo [params] params)
|
||||||
error (mf/use-state false)
|
|
||||||
form (fm/use-form :schema schema:login-form
|
form (fm/use-form :schema schema:login-form
|
||||||
:initial initial)
|
: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)
|
callback-url (:callback-url params)
|
||||||
|
|
||||||
on-error
|
on-error
|
||||||
(fn [cause]
|
(fn [cause]
|
||||||
(let [cause (ex-data cause)]
|
(let [cause (ex-data cause)]
|
||||||
@ -102,12 +109,6 @@
|
|||||||
:else
|
:else
|
||||||
(reset! error (tr "errors.generic")))))
|
(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
|
on-success
|
||||||
(fn [data]
|
(fn [data]
|
||||||
(when (fn? on-success-callback)
|
(when (fn? on-success-callback)
|
||||||
@ -130,7 +131,8 @@
|
|||||||
(->> (rp/cmd! :get-sso-provider {:email (:email params)})
|
(->> (rp/cmd! :get-sso-provider {:email (:email params)})
|
||||||
(rx/map :id)
|
(rx/map :id)
|
||||||
(rx/catch (fn [cause]
|
(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/of nil)))
|
||||||
(rx/subs! (fn [sso-provider-id]
|
(rx/subs! (fn [sso-provider-id]
|
||||||
(if sso-provider-id
|
(if sso-provider-id
|
||||||
@ -139,7 +141,7 @@
|
|||||||
(reset! show-password-field* true))))))))))
|
(reset! show-password-field* true))))))))))
|
||||||
|
|
||||||
on-submit-ldap
|
on-submit-ldap
|
||||||
(mf/use-callback
|
(mf/use-fn
|
||||||
(mf/deps form)
|
(mf/deps form)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
@ -168,91 +170,102 @@
|
|||||||
{:level :error} message])
|
{:level :error} message])
|
||||||
|
|
||||||
[:& fm/form {:on-submit on-submit
|
[:& fm/form {:on-submit on-submit
|
||||||
:class (stl/css :login-form)
|
:class (stl/css :form)
|
||||||
:form form}
|
:form form}
|
||||||
|
|
||||||
[:div {:class (stl/css :fields-row)}
|
[:div {:class (stl/css :form-row)}
|
||||||
[:& fm/input
|
[:& fm/input {:name :email
|
||||||
{:name :email
|
:type "email"
|
||||||
:type "email"
|
:label (tr "auth.work-email")
|
||||||
:label (tr "auth.work-email")
|
:class (stl/css :form-field)}]]
|
||||||
:class (stl/css :form-field)}]]
|
|
||||||
|
|
||||||
(when show-password-field?
|
(when show-password-field?
|
||||||
[:div {:class (stl/css :fields-row)}
|
[:div {:class (stl/css :form-row)}
|
||||||
[:& fm/input
|
[:& fm/input {:type "password"
|
||||||
{:type "password"
|
:name :password
|
||||||
:name :password
|
:auto-focus? true
|
||||||
:auto-focus? true
|
:label (tr "auth.password")
|
||||||
:label (tr "auth.password")
|
:class (stl/css :form-field)}]])
|
||||||
:class (stl/css :form-field)}]])
|
|
||||||
|
|
||||||
(when (and (not= origin :viewer)
|
(when (and (not= origin :viewer)
|
||||||
(or (contains? cf/flags :login)
|
(or (contains? cf/flags :login)
|
||||||
(contains? cf/flags :login-with-password)))
|
(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
|
[:> lk/link* {:action on-recovery-request
|
||||||
:class (stl/css :forgot-pass-link)
|
:class (stl/css :forgot-password-link)
|
||||||
:data-testid "forgot-password"}
|
:data-testid "forgot-password"}
|
||||||
(tr "auth.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)
|
(when (or (contains? cf/flags :login)
|
||||||
(contains? cf/flags :login-with-password))
|
(contains? cf/flags :login-with-password))
|
||||||
[:> fm/submit-button*
|
[:> fm/submit-button* {:label (tr "labels.continue")
|
||||||
{:label (tr "labels.continue")
|
:data-testid "login-submit"
|
||||||
:data-testid "login-submit"
|
:class (stl/css :form-submit-btn)}])
|
||||||
:class (stl/css :login-button)}])
|
|
||||||
|
|
||||||
(when (contains? cf/flags :login-with-ldap)
|
(when (contains? cf/flags :login-with-ldap)
|
||||||
[:> fm/submit-button*
|
[:> fm/submit-button* {:label (tr "auth.login-with-ldap-submit")
|
||||||
{:label (tr "auth.login-with-ldap-submit")
|
:class (stl/css :form-submit-btn)
|
||||||
:class (stl/css :login-ldap-button)
|
:on-click on-submit-ldap}])]]]))
|
||||||
:on-click on-submit-ldap}])]]]))
|
|
||||||
|
|
||||||
(defn raw-icon
|
(defn raw-icon
|
||||||
[id]
|
[id]
|
||||||
(mf/html
|
(mf/html
|
||||||
[:> raw-svg* {:id id :class (stl/css :sso-icon)}]))
|
[:> raw-svg* {:id id :class (stl/css :sso-icon)}]))
|
||||||
|
|
||||||
(mf/defc login-sso-buttons*
|
(mf/defc sso-buttons*
|
||||||
[{:keys [params] :as props}]
|
[{:keys [params] :as props}]
|
||||||
(let [login-with-google (mf/use-fn (mf/deps params) #(login-with-sso "google" params))
|
(let [login-with-google
|
||||||
login-with-github (mf/use-fn (mf/deps params) #(login-with-sso "github" params))
|
(mf/use-fn
|
||||||
login-with-gitlab (mf/use-fn (mf/deps params) #(login-with-sso "gitlab" params))
|
(mf/deps params)
|
||||||
login-with-oidc (mf/use-fn (mf/deps params) #(login-with-sso "oidc" 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)
|
(when (contains? cf/flags :login-with-google)
|
||||||
[:> bl/button-link* {:on-click login-with-google
|
[:> bl/button-link* {:on-click login-with-google
|
||||||
:icon (raw-icon raw-icons/brand-google)
|
:icon (raw-icon raw-icons/brand-google)
|
||||||
:label (tr "auth.login-with-google-submit")
|
: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)
|
(when (contains? cf/flags :login-with-github)
|
||||||
[:> bl/button-link* {:on-click login-with-github
|
[:> bl/button-link* {:on-click login-with-github
|
||||||
:icon (raw-icon raw-icons/brand-github)
|
:icon (raw-icon raw-icons/brand-github)
|
||||||
:label (tr "auth.login-with-github-submit")
|
: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)
|
(when (contains? cf/flags :login-with-gitlab)
|
||||||
[:> bl/button-link* {:on-click login-with-gitlab
|
[:> bl/button-link* {:on-click login-with-gitlab
|
||||||
:icon (raw-icon raw-icons/brand-gitlab)
|
:icon (raw-icon raw-icons/brand-gitlab)
|
||||||
:label (tr "auth.login-with-gitlab-submit")
|
: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)
|
(when (contains? cf/flags :login-with-oidc)
|
||||||
[:> bl/button-link* {:on-click login-with-oidc
|
[:> bl/button-link* {:on-click login-with-oidc
|
||||||
:icon (raw-icon raw-icons/brand-openid)
|
:icon (raw-icon raw-icons/brand-openid)
|
||||||
:label (or (not-empty cf/oidc-name) (tr "auth.login-with-oidc-submit"))
|
: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*
|
(mf/defc login-dialog*
|
||||||
[{:keys [params] :as props}]
|
[{:keys [params] :as props}]
|
||||||
[:*
|
[:*
|
||||||
(when show-sso-login-buttons?
|
(when show-sso-login-buttons?
|
||||||
[:*
|
[:*
|
||||||
[:> login-sso-buttons* {:params params}]
|
[:> sso-buttons* {:params params}]
|
||||||
|
|
||||||
(when (or (contains? cf/flags :login)
|
(when (or (contains? cf/flags :login)
|
||||||
(contains? cf/flags :login-with-password)
|
(contains? cf/flags :login-with-password)
|
||||||
@ -270,11 +283,11 @@
|
|||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
#(st/emit! (rt/nav :auth-register params)))]
|
#(st/emit! (rt/nav :auth-register params)))]
|
||||||
|
|
||||||
[:div {:class (stl/css :auth-form-wrapper)}
|
[:div {:class (stl/css :wrapper)}
|
||||||
[:h1 {:class (stl/css :auth-title)
|
[:h1 {:class (stl/css :title)
|
||||||
:data-testid "login-title"} (tr "auth.login-account-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")]
|
(tr "auth.login-tagline")]
|
||||||
|
|
||||||
(when (contains? cf/flags :demo-warning)
|
(when (contains? cf/flags :demo-warning)
|
||||||
@ -286,7 +299,7 @@
|
|||||||
|
|
||||||
[:div {:class (stl/css :links)}
|
[:div {:class (stl/css :links)}
|
||||||
(when (contains? cf/flags :registration)
|
(when (contains? cf/flags :registration)
|
||||||
[:div {:class (stl/css :register)}
|
[:div {:class (stl/css :register-row)}
|
||||||
[:span {:class (stl/css :register-text)}
|
[:span {:class (stl/css :register-text)}
|
||||||
(tr "auth.register") " "]
|
(tr "auth.register") " "]
|
||||||
[:> lk/link* {:action go-register
|
[:> lk/link* {:action go-register
|
||||||
|
|||||||
@ -6,8 +6,88 @@
|
|||||||
|
|
||||||
@use "./common";
|
@use "./common";
|
||||||
@use "ds/_utils.scss" as *;
|
@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 {
|
.sso-icon {
|
||||||
max-inline-size: px2rem(26);
|
max-inline-size: px2rem(26);
|
||||||
max-block-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)]
|
:initial params)]
|
||||||
|
|
||||||
[:& fm/form {:on-submit on-submit
|
[:& fm/form {:on-submit on-submit
|
||||||
:class (stl/css :recovery-form)
|
:class (stl/css :form)
|
||||||
:form form}
|
:form form}
|
||||||
|
|
||||||
[:div {:class (stl/css :fields-row)}
|
[:div {:class (stl/css :form-row)}
|
||||||
[:& fm/input {:type "password"
|
[:& fm/input {:type "password"
|
||||||
:name :password-1
|
:name :password-1
|
||||||
:show-success? true
|
:show-success? true
|
||||||
:label (tr "auth.new-password")
|
:label (tr "auth.new-password")
|
||||||
:class (stl/css :form-field)}]]
|
:class (stl/css :form-field)}]]
|
||||||
|
|
||||||
[:div {:class (stl/css :fields-row)}
|
[:div {:class (stl/css :form-row)}
|
||||||
[:& fm/input {:type "password"
|
[:& fm/input {:type "password"
|
||||||
:name :password-2
|
:name :password-2
|
||||||
:show-success? true
|
:show-success? true
|
||||||
:label (tr "auth.confirm-password")
|
:label (tr "auth.confirm-password")
|
||||||
:class (stl/css :form-field)}]]
|
:class (stl/css :form-field)}]]
|
||||||
|
|
||||||
[:> fm/submit-button*
|
[:> fm/submit-button* {:label (tr "auth.recovery-submit")
|
||||||
{:label (tr "auth.recovery-submit")
|
:class (stl/css :form-submit-btn)}]]))
|
||||||
:class (stl/css :submit-btn)}]]))
|
|
||||||
|
|
||||||
;; --- Recovery Request Page
|
;; --- Recovery Request Page
|
||||||
|
|
||||||
(mf/defc recovery-page*
|
(mf/defc recovery-page*
|
||||||
[{:keys [params]}]
|
[{:keys [params]}]
|
||||||
[:div {:class (stl/css :auth-form-wrapper)}
|
[:div {:class (stl/css :wrapper)}
|
||||||
[:h1 {:class (stl/css :auth-title)} "Forgot your password?"]
|
[:h1 {:class (stl/css :title)} (tr "auth.recovery-request-title")]
|
||||||
[:div {:class (stl/css :auth-subtitle)} "Please enter your new password"]
|
[:div {:class (stl/css :subtitle)} (tr "auth.recovery-request-subtitle")]
|
||||||
[:hr {:class (stl/css :separator)}]
|
[:hr {:class (stl/css :separator)}]
|
||||||
[:> recovery-form* {:params params}]
|
[:> recovery-form* {:params params}]
|
||||||
|
|
||||||
[:div {:class (stl/css :links)}
|
[: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))
|
[:a {:on-click #(st/emit! (rt/nav :auth-login))
|
||||||
:class (stl/css :go-back-link)}
|
:class (stl/css :go-back-link)}
|
||||||
(tr "profile.recovery.go-to-login")]]]])
|
(tr "profile.recovery.go-to-login")]]]])
|
||||||
|
|||||||
@ -5,7 +5,3 @@
|
|||||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||||
|
|
||||||
@use "./common";
|
@use "./common";
|
||||||
|
|
||||||
.submit-btn {
|
|
||||||
margin-block-start: var(--sp-l);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -70,18 +70,17 @@
|
|||||||
(st/emit! (du/request-profile-recovery params)))))]
|
(st/emit! (du/request-profile-recovery params)))))]
|
||||||
|
|
||||||
[:& fm/form {:on-submit on-submit
|
[:& fm/form {:on-submit on-submit
|
||||||
:class (stl/css :recovery-request-form)
|
:class (stl/css :form)
|
||||||
:form form}
|
:form form}
|
||||||
[:div {:class (stl/css :fields-row)}
|
[:div {:class (stl/css :form-row)}
|
||||||
[:& fm/input {:name :email
|
[:& fm/input {:name :email
|
||||||
:label (tr "auth.work-email")
|
:label (tr "auth.work-email")
|
||||||
:type "text"
|
:type "text"
|
||||||
:class (stl/css :form-field)}]]
|
:class (stl/css :form-field)}]]
|
||||||
|
|
||||||
[:> fm/submit-button*
|
[:> fm/submit-button* {:label (tr "auth.recovery-request-submit")
|
||||||
{:label (tr "auth.recovery-request-submit")
|
:data-testid "recovery-resquest-submit"
|
||||||
:data-testid "recovery-resquest-submit"
|
:class (stl/css :form-submit-btn)}]]))
|
||||||
:class (stl/css :recover-btn)}]]))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- Recovery Request Page
|
;; --- Recovery Request Page
|
||||||
@ -90,14 +89,14 @@
|
|||||||
[{:keys [params on-success-callback go-back-callback]}]
|
[{:keys [params on-success-callback go-back-callback]}]
|
||||||
(let [default-go-back #(st/emit! (rt/nav :auth-login))
|
(let [default-go-back #(st/emit! (rt/nav :auth-login))
|
||||||
go-back (or go-back-callback default-go-back)]
|
go-back (or go-back-callback default-go-back)]
|
||||||
[:div {:class (stl/css :auth-form-wrapper)}
|
[:div {:class (stl/css :wrapper)}
|
||||||
[:h1 {:class (stl/css :auth-title)} (tr "auth.recovery-request-title")]
|
[:h1 {:class (stl/css :title)} (tr "auth.recovery-request-title")]
|
||||||
[:div {:class (stl/css :auth-subtitle)} (tr "auth.recovery-request-subtitle")]
|
[:div {:class (stl/css :subtitle)} (tr "auth.recovery-request-subtitle")]
|
||||||
[:hr {:class (stl/css :separator)}]
|
[:hr {:class (stl/css :separator)}]
|
||||||
|
|
||||||
[:> recovery-form* {:params params :on-success-callback on-success-callback}]
|
[:> recovery-form* {:params params :on-success-callback on-success-callback}]
|
||||||
[:hr {:class (stl/css :separator)}]
|
[:hr {:class (stl/css :separator)}]
|
||||||
[:div {:class (stl/css :go-back)}
|
[:div {:class (stl/css :go-back-row)}
|
||||||
[:> lk/link* {:action go-back
|
[:> lk/link* {:action go-back
|
||||||
:class (stl/css :go-back-link)
|
:class (stl/css :go-back-link)
|
||||||
:data-testid "go-back-link"}
|
:data-testid "go-back-link"}
|
||||||
@ -106,11 +105,10 @@
|
|||||||
|
|
||||||
(mf/defc recovery-sent-page*
|
(mf/defc recovery-sent-page*
|
||||||
[{:keys [email]}]
|
[{:keys [email]}]
|
||||||
[:div {:class (stl/css :auth-form-wrapper :register-success)}
|
[:div {:class (stl/css :wrapper :register-success)}
|
||||||
[:div {:class (stl/css :auth-title-wrapper)}
|
[:div {:class (stl/css :title-wrapper)}
|
||||||
[:h2 {:class (stl/css :auth-title)}
|
[:h2 {:class (stl/css :title)}
|
||||||
(tr "auth.check-email")]
|
(tr "auth.check-email")]
|
||||||
[:div {:class (stl/css :notification-text)} (tr "not-found.login.sent-recovery")]]
|
[: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")]])
|
[:div {:class (stl/css :notification-text)} (tr "not-found.login.sent-recovery-check")]])
|
||||||
|
|
||||||
|
|||||||
@ -4,19 +4,4 @@
|
|||||||
//
|
//
|
||||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||||
|
|
||||||
@use "ds/_utils.scss" as *;
|
|
||||||
@use "ds/_sizes.scss" as *;
|
|
||||||
@use "ds/typography.scss" as *;
|
|
||||||
@use "./common";
|
@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
|
;; --- PAGE: Register
|
||||||
|
|
||||||
(mf/defc newsletter-options*
|
(mf/defc newsletter-checkbox*
|
||||||
{::mf/private true}
|
{::mf/private true}
|
||||||
[]
|
[]
|
||||||
(let [updates-label
|
(let [newsletter-label
|
||||||
(mf/html
|
(mf/html
|
||||||
[:> i18n/tr-html*
|
[:> i18n/tr-html* {:tag-name "div"
|
||||||
{:tag-name "div"
|
:content (tr "onboarding-v2.newsletter.updates")}])]
|
||||||
:content (tr "onboarding-v2.newsletter.updates")}])]
|
|
||||||
[:div {:class (stl/css :fields-row :input-visible :newsletter-option-wrapper)}
|
[:div {:class (stl/css :form-row :input-visible :newsletter-option-wrapper)}
|
||||||
[:& fm/input {:name :accept-newsletter-updates
|
[:& fm/input {:name :accept-newsletter-updates
|
||||||
:class (stl/css :checkbox-newsletter-updates)
|
:class (stl/css :checkbox-newsletter-updates)
|
||||||
:type "checkbox"
|
:type "checkbox"
|
||||||
:default-checked false
|
:default-checked false
|
||||||
:label updates-label}]]))
|
:label newsletter-label}]]))
|
||||||
|
|
||||||
(mf/defc terms-and-privacy*
|
(mf/defc terms-and-privacy-checkbox*
|
||||||
{::mf/private true}
|
{::mf/private true}
|
||||||
[]
|
[]
|
||||||
(let [terms-label
|
(let [terms-and-privacy-label
|
||||||
(mf/html
|
(mf/html
|
||||||
[:> i18n/tr-html*
|
[:> i18n/tr-html* {:tag-name "div"
|
||||||
{:tag-name "div"
|
:content (tr "auth.terms-and-privacy-agreement"
|
||||||
:content (tr "auth.terms-and-privacy-agreement"
|
cf/terms-of-service-uri
|
||||||
cf/terms-of-service-uri
|
cf/privacy-policy-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
|
[:& fm/input {:name :accept-terms-and-privacy
|
||||||
:show-error false
|
:show-error false
|
||||||
:class (stl/css :checkbox-terms-and-privacy)
|
:class (stl/css :checkbox-terms-and-privacy)
|
||||||
:type "checkbox"
|
:type "checkbox"
|
||||||
:default-checked false
|
:default-checked false
|
||||||
:label terms-label}]]))
|
:label terms-and-privacy-label}]]))
|
||||||
|
|
||||||
(def ^:private schema:register-form
|
(def ^:private schema:register-form
|
||||||
[:map {:title "RegisterForm"}
|
[:map {:title "RegisterForm"}
|
||||||
@ -76,8 +75,7 @@
|
|||||||
form (fm/use-form :schema schema:register-form
|
form (fm/use-form :schema schema:register-form
|
||||||
:initial initial)
|
:initial initial)
|
||||||
|
|
||||||
submitted?
|
submitted? (mf/use-state false)
|
||||||
(mf/use-state false)
|
|
||||||
|
|
||||||
on-error
|
on-error
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -167,22 +165,24 @@
|
|||||||
(->> (rp/cmd! :prepare-register-profile cdata)
|
(->> (rp/cmd! :prepare-register-profile cdata)
|
||||||
(rx/subs! on-register-profile on-error #(reset! submitted? false))))))]
|
(rx/subs! on-register-profile on-error #(reset! submitted? false))))))]
|
||||||
|
|
||||||
[:& fm/form {:on-submit on-submit :form form}
|
[:& fm/form {:on-submit on-submit
|
||||||
[:div {:class (stl/css :fields-row)}
|
:form form
|
||||||
|
:class (stl/css :form)}
|
||||||
|
[:div {:class (stl/css :form-row)}
|
||||||
|
|
||||||
[:& fm/input {:name :fullname
|
[:& fm/input {:name :fullname
|
||||||
:label (tr "auth.fullname")
|
:label (tr "auth.fullname")
|
||||||
:type "text"
|
:type "text"
|
||||||
:show-success? true
|
:show-success? true
|
||||||
:class (stl/css :form-field)}]]
|
:class (stl/css :form-field)}]]
|
||||||
[:div {:class (stl/css :fields-row)}
|
[:div {:class (stl/css :form-row)}
|
||||||
[:& fm/input {:type "text"
|
[:& fm/input {:type "text"
|
||||||
:name :email
|
:name :email
|
||||||
:label (tr "auth.work-email")
|
:label (tr "auth.work-email")
|
||||||
:data-testid "email-input"
|
:data-testid "email-input"
|
||||||
:show-success? true
|
:show-success? true
|
||||||
:class (stl/css :form-field)}]]
|
:class (stl/css :form-field)}]]
|
||||||
[:div {:class (stl/css :fields-row)}
|
[:div {:class (stl/css :form-row)}
|
||||||
[:& fm/input {:name :password
|
[:& fm/input {:name :password
|
||||||
:hint (tr "auth.password-length-hint")
|
:hint (tr "auth.password-length-hint")
|
||||||
:label (tr "auth.password")
|
:label (tr "auth.password")
|
||||||
@ -191,21 +191,20 @@
|
|||||||
:class (stl/css :form-field)}]]
|
:class (stl/css :form-field)}]]
|
||||||
|
|
||||||
(when (contains? cf/flags :terms-and-privacy-checkbox)
|
(when (contains? cf/flags :terms-and-privacy-checkbox)
|
||||||
[:> terms-and-privacy*])
|
[:> terms-and-privacy-checkbox*])
|
||||||
|
|
||||||
[:> newsletter-options*]
|
[:> newsletter-checkbox*]
|
||||||
|
|
||||||
[:> fm/submit-button*
|
[:> fm/submit-button* {:label (tr "auth.register-submit")
|
||||||
{:label (tr "auth.register-submit")
|
:disabled @submitted?
|
||||||
:disabled @submitted?
|
:data-testid "register-form-submit"
|
||||||
:data-testid "register-form-submit"
|
:class (stl/css :form-submit-btn)}]]))
|
||||||
:class (stl/css :register-btn)}]]))
|
|
||||||
|
|
||||||
(mf/defc register-methods*
|
(mf/defc register-methods*
|
||||||
[{:keys [params hide-separator on-success-callback]}]
|
[{:keys [params hide-separator on-success-callback]}]
|
||||||
[:*
|
[:*
|
||||||
(when login/show-sso-login-buttons?
|
(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))
|
(when (or login/show-sso-login-buttons? (false? hide-separator))
|
||||||
[:hr {:class (stl/css :separator)}])
|
[:hr {:class (stl/css :separator)}])
|
||||||
(when (contains? cf/flags :login-with-password)
|
(when (contains? cf/flags :login-with-password)
|
||||||
@ -213,8 +212,8 @@
|
|||||||
|
|
||||||
(mf/defc register-page*
|
(mf/defc register-page*
|
||||||
[{:keys [params]}]
|
[{:keys [params]}]
|
||||||
[:div {:class (stl/css :auth-form-wrapper :register-form)}
|
[:div {:class (stl/css :wrapper :register-form)}
|
||||||
[:h1 {:class (stl/css :auth-title)
|
[:h1 {:class (stl/css :title)
|
||||||
:data-testid "registration-title"} (tr "auth.register-title")]
|
:data-testid "registration-title"} (tr "auth.register-title")]
|
||||||
|
|
||||||
(when (contains? cf/flags :demo-warning)
|
(when (contains? cf/flags :demo-warning)
|
||||||
@ -223,7 +222,7 @@
|
|||||||
[:> register-methods* {:params params}]
|
[:> register-methods* {:params params}]
|
||||||
|
|
||||||
[:div {:class (stl/css :links)}
|
[: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") " "]
|
[:span {:class (stl/css :account-text)} (tr "auth.already-have-account") " "]
|
||||||
[:> lk/link* {:action #(st/emit! (rt/nav :auth-login params))
|
[:> lk/link* {:action #(st/emit! (rt/nav :auth-login params))
|
||||||
:class (stl/css :account-link)
|
:class (stl/css :account-link)
|
||||||
@ -233,9 +232,9 @@
|
|||||||
(when (contains? cf/flags :demo-users)
|
(when (contains? cf/flags :demo-users)
|
||||||
[:*
|
[:*
|
||||||
[:hr {:class (stl/css :separator)}]
|
[:hr {:class (stl/css :separator)}]
|
||||||
[:div {:class (stl/css :demo-account)}
|
[:div {:class (stl/css :account-row)}
|
||||||
[:> lk/link* {:action login/create-demo-profile
|
[:> lk/link* {:action login/create-demo-profile
|
||||||
:class (stl/css :demo-account-link)}
|
:class (stl/css :account-link)}
|
||||||
(tr "auth.create-demo-account")]]])]])
|
(tr "auth.create-demo-account")]]])]])
|
||||||
|
|
||||||
|
|
||||||
@ -244,31 +243,31 @@
|
|||||||
(mf/defc register-success-page*
|
(mf/defc register-success-page*
|
||||||
[{:keys [params]}]
|
[{:keys [params]}]
|
||||||
(let [email (or (:email params) (::email storage/user))]
|
(let [email (or (:email params) (::email storage/user))]
|
||||||
[:div {:class (stl/css :auth-form-wrapper :register-success)}
|
[:div {:class (stl/css :wrapper :register-success)}
|
||||||
[:div {:class (stl/css :auth-title-wrapper)}
|
[:div {:class (stl/css :title-wrapper)}
|
||||||
[:h2 {:class (stl/css :auth-title)}
|
[:h2 {:class (stl/css :register-success-title)}
|
||||||
(tr "auth.check-email")]
|
(tr "auth.check-email")]
|
||||||
[:div {:class (stl/css :notification-text)} (tr "auth.verification-sent-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)
|
(let [show-all? (and cf/terms-of-service-uri cf/privacy-policy-uri)
|
||||||
show-terms? (some? cf/terms-of-service-uri)
|
show-terms? (some? cf/terms-of-service-uri)
|
||||||
show-privacy? (some? cf/privacy-policy-uri)]
|
show-privacy? (some? cf/privacy-policy-uri)]
|
||||||
|
|
||||||
(when show-all?
|
(when show-all?
|
||||||
[:div {:class (stl/css :terms-register)}
|
[:div {:class (stl/css :terms)}
|
||||||
(when show-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")])
|
(tr "auth.terms-of-service")])
|
||||||
|
|
||||||
(when show-all?
|
(when show-all?
|
||||||
[:span {:class (stl/css :and-text)}
|
[:span {:class (stl/css :terms-and)}
|
||||||
(dm/str " " (tr "labels.and") " ")])
|
(dm/str " " (tr "labels.and") " ")])
|
||||||
|
|
||||||
(when show-privacy?
|
(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")])])))
|
(tr "auth.privacy-policy")])])))
|
||||||
|
|
||||||
;; --- PAGE: register validation
|
;; --- PAGE: register validation
|
||||||
@ -334,9 +333,9 @@
|
|||||||
|
|
||||||
[:& fm/form {:on-submit on-submit
|
[:& fm/form {:on-submit on-submit
|
||||||
:form form
|
: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
|
[:& fm/input {:name :fullname
|
||||||
:label (tr "auth.fullname")
|
:label (tr "auth.fullname")
|
||||||
:type "text"
|
:type "text"
|
||||||
@ -344,30 +343,28 @@
|
|||||||
:class (stl/css :form-field)}]]
|
:class (stl/css :form-field)}]]
|
||||||
|
|
||||||
(when (contains? cf/flags :terms-and-privacy-checkbox)
|
(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?
|
|
||||||
:class (stl/css :register-btn)}]]))
|
|
||||||
|
|
||||||
|
[:> fm/submit-button* {:label (tr "auth.register-submit")
|
||||||
|
:disabled @submitted?
|
||||||
|
:class (stl/css :form-submit-btn)}]]))
|
||||||
|
|
||||||
(mf/defc register-validate-page*
|
(mf/defc register-validate-page*
|
||||||
[{:keys [params]}]
|
[{: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)}
|
[:div {:class (stl/css :title-wrapper)}
|
||||||
[:h2 {:class (stl/css :auth-title)
|
[:h2 {:class (stl/css :title)
|
||||||
:data-testid "register-title"} (tr "auth.register-account-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}]
|
[:> register-validate-form* {:params params}]
|
||||||
|
|
||||||
[:div {:class (stl/css :links)}
|
[: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 {}))
|
[:> lk/link* {:action #(st/emit! (rt/nav :auth-register {}))
|
||||||
:class (stl/css :go-back-link)}
|
:class (stl/css :go-back-link)}
|
||||||
(tr "labels.go-back")]]]])
|
(tr "labels.go-back")]]]])
|
||||||
|
|||||||
@ -4,57 +4,64 @@
|
|||||||
//
|
//
|
||||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||||
|
|
||||||
|
@use "./common";
|
||||||
@use "ds/_utils.scss" as *;
|
@use "ds/_utils.scss" as *;
|
||||||
@use "ds/_sizes.scss" as *;
|
@use "ds/_sizes.scss" as *;
|
||||||
@use "ds/_borders.scss" as *;
|
@use "ds/_borders.scss" as *;
|
||||||
@use "ds/typography.scss" as *;
|
@use "ds/typography.scss" as *;
|
||||||
@use "./common";
|
|
||||||
|
|
||||||
.checkbox-terms-and-privacy,
|
|
||||||
.checkbox-newsletter-updates {
|
.checkbox-newsletter-updates {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkbox-terms-and-privacy {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.register-form {
|
.register-form {
|
||||||
gap: var(--sp-xxl);
|
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 {
|
.register-success {
|
||||||
gap: var(--sp-xxl);
|
gap: var(--sp-xxl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.register-success .auth-title {
|
.register-success-title {
|
||||||
@include use-typography("title-medium");
|
@include use-typography("title-medium");
|
||||||
|
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
color: var(--color-foreground-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-text {
|
.terms {
|
||||||
@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 {
|
|
||||||
@include use-typography("body-small");
|
@include use-typography("body-small");
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -63,15 +70,15 @@
|
|||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.and-text {
|
.terms-link {
|
||||||
border-block-end: $b-1 solid transparent;
|
|
||||||
color: var(--color-foreground-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-link {
|
|
||||||
color: var(--color-accent-primary);
|
color: var(--color-accent-primary);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.terms-and {
|
||||||
|
border-block-end: $b-1 solid transparent;
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
}
|
||||||
|
|||||||
@ -114,6 +114,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-block-size: 0;
|
||||||
outline: $b-1 solid var(--tab-panel-outline-color);
|
outline: $b-1 solid var(--tab-panel-outline-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -185,7 +185,7 @@
|
|||||||
:on-click set-section} (tr "auth.login-here")]]
|
:on-click set-section} (tr "auth.login-here")]]
|
||||||
[:div {:class (stl/css :links)}
|
[:div {:class (stl/css :links)}
|
||||||
[:hr {:class (stl/css :separator)}]
|
[:hr {:class (stl/css :separator)}]
|
||||||
[:> register/terms-register*]]]
|
[:> register/terms-service-privacy-policy*]]]
|
||||||
|
|
||||||
:register-validate
|
:register-validate
|
||||||
[:div {:class (stl/css :form-container)}
|
[:div {:class (stl/css :form-container)}
|
||||||
|
|||||||
@ -13,8 +13,10 @@
|
|||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.auth.login :refer [login-dialog*]]
|
[app.main.ui.auth.login :refer [login-dialog*]]
|
||||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page*]]
|
[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.auth.register :refer [register-methods* register-success-page*
|
||||||
[app.main.ui.icons :as deprecated-icon]
|
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.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
@ -25,14 +27,15 @@
|
|||||||
{::mf/register modal/components
|
{::mf/register modal/components
|
||||||
::mf/register-as :login-register}
|
::mf/register-as :login-register}
|
||||||
[_]
|
[_]
|
||||||
(let [user-email (mf/use-state "")
|
(let [user-email (mf/use-state "")
|
||||||
register-token (mf/use-state "")
|
register-token (mf/use-state "")
|
||||||
|
|
||||||
current-section* (mf/use-state :login)
|
current-section* (mf/use-state :login)
|
||||||
current-section (deref current-section*)
|
current-section (deref current-section*)
|
||||||
|
|
||||||
set-current-section
|
set-current-section
|
||||||
(mf/use-fn #(reset! current-section* %))
|
(mf/use-fn
|
||||||
|
#(reset! current-section* %))
|
||||||
|
|
||||||
set-section
|
set-section
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -42,7 +45,9 @@
|
|||||||
(keyword))]
|
(keyword))]
|
||||||
(set-current-section section))))
|
(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
|
main-section (or
|
||||||
(= current-section :login)
|
(= current-section :login)
|
||||||
@ -52,13 +57,16 @@
|
|||||||
(fn [event]
|
(fn [event]
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
(st/emit! (modal/hide)))
|
(st/emit! (modal/hide)))
|
||||||
|
|
||||||
success-email-sent
|
success-email-sent
|
||||||
(fn [email]
|
(fn [email]
|
||||||
(reset! user-email email)
|
(reset! user-email email)
|
||||||
(set-current-section :email-sent))
|
(set-current-section :email-sent))
|
||||||
|
|
||||||
success-login
|
success-login
|
||||||
(fn []
|
(fn []
|
||||||
(.reload js/window.location true))
|
(.reload js/window.location true))
|
||||||
|
|
||||||
success-register
|
success-register
|
||||||
(fn [data]
|
(fn [data]
|
||||||
(reset! register-token (:token data))
|
(reset! register-token (:token data))
|
||||||
@ -67,49 +75,48 @@
|
|||||||
[:div {:class (stl/css :modal-overlay)}
|
[:div {:class (stl/css :modal-overlay)}
|
||||||
[:div {:class (stl/css :modal-container)}
|
[:div {:class (stl/css :modal-container)}
|
||||||
[:div {:class (stl/css :modal-header)}
|
[:div {:class (stl/css :modal-header)}
|
||||||
[:h2 {:class (stl/css :modal-title)} (tr "labels.continue-with-penpot")]
|
[:h2 {:class (stl/css :modal-header-title)} (tr "labels.continue-with-penpot")]
|
||||||
[:button {:class (stl/css :modal-close-btn)
|
[:> icon-button* {:variant "ghost"
|
||||||
:title (tr "labels.close")
|
:class (stl/css :modal-close)
|
||||||
:on-click close} deprecated-icon/close]]
|
:aria-label (tr "labels.close")
|
||||||
|
:on-click close
|
||||||
|
:icon i/close}]]
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-content)}
|
[:div {:class (stl/css :modal-content)}
|
||||||
(case current-section
|
(case current-section
|
||||||
:login
|
:login
|
||||||
[:div {:class (stl/css :form-container)}
|
[:div {:class (stl/css :login-form)}
|
||||||
[:> login-dialog*
|
[:> login-dialog* {:on-success-callback success-login
|
||||||
{:on-success-callback success-login
|
:origin :viewer}]
|
||||||
:origin :viewer}]
|
[:div {:class (stl/css :login-links)}
|
||||||
[:div {:class (stl/css :links)}
|
[:div
|
||||||
[:div {:class (stl/css :recovery-request)}
|
|
||||||
[:a {:on-click set-section
|
[:a {:on-click set-section
|
||||||
:class (stl/css :recovery-link)
|
|
||||||
:data-value "recovery-request"}
|
:data-value "recovery-request"}
|
||||||
(tr "auth.forgot-password")]]
|
(tr "auth.forgot-password")]]
|
||||||
(when (contains? cf/flags :registration)
|
(when (contains? cf/flags :registration)
|
||||||
[:div {:class (stl/css :register)}
|
[:div
|
||||||
[:span {:class (stl/css :register-text)}
|
[:span
|
||||||
(tr "auth.register") " "]
|
(tr "auth.register") " "]
|
||||||
[:a {:on-click set-section
|
[:a {:on-click set-section
|
||||||
:class (stl/css :register-link)
|
|
||||||
:data-value "register"}
|
:data-value "register"}
|
||||||
(tr "auth.register-submit")]])]]
|
(tr "auth.register-submit")]])]]
|
||||||
|
|
||||||
:register
|
:register
|
||||||
[:div {:class (stl/css :form-container)}
|
[:div {:class (stl/css :login-form)}
|
||||||
[:> register-methods* {:on-success-callback success-register}]
|
[:> register-methods* {:on-success-callback success-register}]
|
||||||
[:div {:class (stl/css :links)}
|
[:div {:class (stl/css :login-links)}
|
||||||
[:div {:class (stl/css :account)}
|
[:div
|
||||||
[:span (tr "auth.already-have-account") " "]
|
[:span (tr "auth.already-have-account") " "]
|
||||||
[:a {:on-click set-section
|
[:a {:on-click set-section
|
||||||
:data-value "login"}
|
:data-value "login"}
|
||||||
(tr "auth.login-here")]]]]
|
(tr "auth.login-here")]]]]
|
||||||
|
|
||||||
:register-validate
|
:register-validate
|
||||||
[:div {:class (stl/css :form-container)}
|
[:div {:class (stl/css :login-form)}
|
||||||
[:> register-validate-form* {:params {:token @register-token}
|
[:> register-validate-form* {:params {:token @register-token}
|
||||||
:on-success-callback success-email-sent}]
|
:on-success-callback success-email-sent}]
|
||||||
[:div {:class (stl/css :links)}
|
[:div {:class (stl/css :login-links)}
|
||||||
[:div {:class (stl/css :register)}
|
[:div
|
||||||
[:a {:on-click set-section
|
[:a {:on-click set-section
|
||||||
:data-value "register"}
|
:data-value "register"}
|
||||||
(tr "labels.go-back")]]]]
|
(tr "labels.go-back")]]]]
|
||||||
@ -117,10 +124,11 @@
|
|||||||
:recovery-request
|
:recovery-request
|
||||||
[:> recovery-request-page* {:go-back-callback go-back-to-login
|
[:> recovery-request-page* {:go-back-callback go-back-to-login
|
||||||
:on-success-callback success-email-sent}]
|
:on-success-callback success-email-sent}]
|
||||||
|
|
||||||
:email-sent
|
:email-sent
|
||||||
[:div {:class (stl/css :form-container)}
|
[:div {:class (stl/css :login-form)}
|
||||||
[:> register-success-page* {:params {:email @user-email}}]])
|
[:> register-success-page* {:params {:email @user-email}}]])
|
||||||
|
|
||||||
(when main-section
|
(when main-section
|
||||||
[:div {:class (stl/css :links)}
|
[:div {:class (stl/css :login-links)}
|
||||||
[:> terms-register*]])]]]))
|
[:> terms-service-privacy-policy*]])]]]))
|
||||||
|
|||||||
@ -4,75 +4,71 @@
|
|||||||
//
|
//
|
||||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
// 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 {
|
.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 {
|
.modal-container {
|
||||||
@extend %modal-container-base;
|
position: relative;
|
||||||
|
padding: var(--sp-xxxl);
|
||||||
width: deprecated.$s-368;
|
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 {
|
.modal-header {
|
||||||
margin-bottom: deprecated.$s-24;
|
margin-block-end: var(--sp-xxl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-title {
|
.modal-header-title {
|
||||||
@include deprecated.uppercase-title-typography;
|
@include use-typography("headline-small");
|
||||||
|
|
||||||
color: var(--modal-title-foreground-color);
|
color: var(--color-foreground-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close-btn {
|
.modal-close {
|
||||||
@extend %modal-close-btn-base;
|
position: absolute;
|
||||||
|
inset-block-start: var(--sp-s);
|
||||||
|
inset-inline-end: var(--sp-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
@include deprecated.flex-column;
|
@include use-typography("body-small");
|
||||||
@include deprecated.body-small-typography;
|
|
||||||
|
|
||||||
gap: deprecated.$s-24;
|
display: flex;
|
||||||
max-height: deprecated.$s-400;
|
flex-direction: column;
|
||||||
|
gap: var(--sp-xxl);
|
||||||
|
max-block-size: px2rem(576);
|
||||||
overflow: hidden auto;
|
overflow: hidden auto;
|
||||||
|
|
||||||
form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-container {
|
.login-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: var(--sp-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.links {
|
.login-links {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
color: var(--color-foreground-primary);
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,9 +97,7 @@
|
|||||||
sitemap-height
|
sitemap-height
|
||||||
(if sitemap-collapsed? 32 height)]
|
(if sitemap-collapsed? 32 height)]
|
||||||
|
|
||||||
[:article {:class (stl/css :layers-tab)
|
[:article {:class (stl/css :layers-tab)}
|
||||||
:style {:--height (dm/str height "px")}}
|
|
||||||
|
|
||||||
[:> sitemap* {:layout layout
|
[:> sitemap* {:layout layout
|
||||||
:height sitemap-height
|
:height sitemap-height
|
||||||
:collapsed sitemap-collapsed?
|
:collapsed sitemap-collapsed?
|
||||||
|
|||||||
@ -79,13 +79,15 @@
|
|||||||
|
|
||||||
.layers-tab {
|
.layers-tab {
|
||||||
padding-block-start: var(--sp-xs);
|
padding-block-start: var(--sp-xs);
|
||||||
overflow-x: hidden;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-block-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layers-tab-resize-area {
|
.layers-tab-resize-area {
|
||||||
background-color: var(--color-background-primary);
|
background-color: var(--color-background-primary);
|
||||||
position: absolute;
|
flex: 0 0 auto;
|
||||||
inset-inline-start: 0;
|
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
padding: px2rem(3) 0 px2rem(1);
|
padding: px2rem(3) 0 px2rem(1);
|
||||||
block-size: $sz-6;
|
block-size: $sz-6;
|
||||||
@ -120,11 +122,16 @@
|
|||||||
.left-sidebar-content {
|
.left-sidebar-content {
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
inset-inline-end: calc(-1 * var(--sp-s));
|
inset-inline-end: calc(-1 * var(--sp-s));
|
||||||
|
min-block-size: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-sidebar-tabs {
|
.left-sidebar-tabs {
|
||||||
--tabs-nav-padding-inline-start: var(--sp-m);
|
--tabs-nav-padding-inline-start: var(--sp-m);
|
||||||
--tabs-nav-padding-inline-end: var(--sp-m);
|
--tabs-nav-padding-inline-end: var(--sp-m);
|
||||||
|
|
||||||
|
block-size: 100%;
|
||||||
|
min-block-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-sidebar-resize-area {
|
.left-sidebar-resize-area {
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: var(--sp-xs);
|
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 {
|
.tool-window-bar-title {
|
||||||
@ -31,11 +31,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-window-content {
|
.tool-window-content {
|
||||||
--calculated-height: calc(#{px2rem(136)} + var(--height, #{$sz-200}));
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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));
|
inline-size: calc(var(--left-sidebar-width) + var(--depth) * var(--layer-indentation-size));
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
@ -117,6 +116,10 @@
|
|||||||
|
|
||||||
.layers {
|
.layers {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-block-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.replace-wrapper {
|
.replace-wrapper {
|
||||||
|
|||||||
@ -15,7 +15,8 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 0 0 auto;
|
||||||
|
flex-shrink: 0;
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
block-size: var(--height, $sz-200);
|
block-size: var(--height, $sz-200);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -150,16 +150,26 @@
|
|||||||
|
|
||||||
on-main-key-down
|
on-main-key-down
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
(mf/deps open)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(cond
|
(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
|
(do
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
(cancel-timer! close-timer*)
|
(cancel-timer! close-timer*)
|
||||||
(reset! open* true))
|
(reset! open* true)))))
|
||||||
|
|
||||||
(and open (kbd/esc? event))
|
|
||||||
(reset! open* false))))
|
|
||||||
|
|
||||||
on-flyout-key-down
|
on-flyout-key-down
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -219,7 +229,10 @@
|
|||||||
:aria-expanded open
|
:aria-expanded open
|
||||||
:has-tooltip false
|
:has-tooltip false
|
||||||
:icon default-icon
|
: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
|
:on-key-down on-main-key-down
|
||||||
:data-tool (name default-tool)}]
|
:data-tool (name default-tool)}]
|
||||||
|
|
||||||
|
|||||||
@ -282,8 +282,7 @@
|
|||||||
[code value]
|
[code value]
|
||||||
(if (some? 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: " value ". Code: " code)))
|
||||||
(throw (js/Error. (dm/str "[PENPOT PLUGIN] Value not valid. Code: " code))))
|
(throw (js/Error. (dm/str "[PENPOT PLUGIN] Value not valid. Code: " code)))))
|
||||||
nil)
|
|
||||||
|
|
||||||
(defn not-valid
|
(defn not-valid
|
||||||
[plugin-id code value]
|
[plugin-id code value]
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
"@types/node": "^26.1.2",
|
"@types/node": "^26.1.2",
|
||||||
"@vitest/browser": "^4.1.10",
|
"@vitest/browser": "^4.1.10",
|
||||||
"@vitest/coverage-v8": "^4.1.10",
|
"@vitest/coverage-v8": "^4.1.10",
|
||||||
"@vitest/ui": "^4.1.10",
|
"@vitest/ui": "^4.1.11",
|
||||||
"canvas": "^3.2.3",
|
"canvas": "^3.2.3",
|
||||||
"esbuild": "^0.28.0",
|
"esbuild": "^0.28.0",
|
||||||
"jsdom": "^30.0.1",
|
"jsdom": "^30.0.1",
|
||||||
@ -28,5 +28,5 @@
|
|||||||
"vite": "^8.2.0",
|
"vite": "^8.2.0",
|
||||||
"vitest": "^4.1.10"
|
"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",
|
"version": "1.2.0-RC1",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"author": "Kaleidos INC Sucursal en España SL",
|
"author": "Kaleidos INC Sucursal en España SL",
|
||||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -36,7 +36,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^26.1.2",
|
"@types/node": "^26.1.2",
|
||||||
"@zip.js/zip.js": "2.8.34",
|
"@zip.js/zip.js": "2.8.34",
|
||||||
"concurrently": "^10.0.4",
|
"concurrently": "^10.0.5",
|
||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.4.0",
|
||||||
"nodemon": "^3.1.14",
|
"nodemon": "^3.1.14",
|
||||||
"source-map-support": "^0.5.21"
|
"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'
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
@ -18,8 +119,8 @@ importers:
|
|||||||
specifier: 2.8.34
|
specifier: 2.8.34
|
||||||
version: 2.8.34(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95)
|
version: 2.8.34(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95)
|
||||||
concurrently:
|
concurrently:
|
||||||
specifier: ^10.0.4
|
specifier: ^10.0.5
|
||||||
version: 10.0.4
|
version: 10.0.5
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^4.4.0
|
specifier: ^4.4.0
|
||||||
version: 4.4.0
|
version: 4.4.0
|
||||||
@ -82,8 +183,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
|
resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
concurrently@10.0.4:
|
concurrently@10.0.5:
|
||||||
resolution: {integrity: sha512-trZql+7l/0+WRAsAnEdctr4+iiOS6ZrViI6H8QWcCF9MFS/LT0dKpe8vluB1to6it+OxSI4VospFTIFMW8DJRw==}
|
resolution: {integrity: sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@ -304,7 +405,7 @@ snapshots:
|
|||||||
strip-ansi: 7.2.0
|
strip-ansi: 7.2.0
|
||||||
wrap-ansi: 9.0.2
|
wrap-ansi: 9.0.2
|
||||||
|
|
||||||
concurrently@10.0.4:
|
concurrently@10.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
rxjs: 7.8.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
|
# those stale values would leak into substitution. And because Docker Compose
|
||||||
# gives shell-env precedence over --env-file, the re-injected per-instance
|
# gives shell-env precedence over --env-file, the re-injected per-instance
|
||||||
# overrides cleanly override the defaults.env baseline. Re-injected: HOME/PATH
|
# overrides cleanly override the defaults.env baseline. Re-injected: HOME/PATH
|
||||||
# (tooling), CURRENT_USER_ID/PENPOT_SOURCE_PATH (always per-call), and the
|
# (tooling), CURRENT_USER_ID/PENPOT_SOURCE_PATH (always per-call), the
|
||||||
# instance-env-overrides block.
|
# 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 {
|
function infra-compose {
|
||||||
env -i HOME="$HOME" PATH="$PATH" PWD="$PWD" \
|
env -i HOME="$HOME" PATH="$PATH" PWD="$PWD" \
|
||||||
docker compose -p penpotdev-infra \
|
docker compose -p penpotdev-infra \
|
||||||
@ -245,14 +247,26 @@ function instance-compose {
|
|||||||
# Per-instance overrides apply to all workspaces uniformly.
|
# Per-instance overrides apply to all workspaces uniformly.
|
||||||
mapfile -t overrides < <(instance-env-overrides "$instance")
|
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" \
|
env -i HOME="$HOME" PATH="$PATH" PWD="$PWD" \
|
||||||
CURRENT_USER_ID="${CURRENT_USER_ID:-$(id -u)}" \
|
CURRENT_USER_ID="${CURRENT_USER_ID:-$(id -u)}" \
|
||||||
PENPOT_SOURCE_PATH="$source_path" \
|
PENPOT_SOURCE_PATH="$source_path" \
|
||||||
DEVENV_TAG="$DEVENV_TAG" \
|
DEVENV_TAG="$DEVENV_TAG" \
|
||||||
|
"${opencode_env[@]}" \
|
||||||
"${overrides[@]}" \
|
"${overrides[@]}" \
|
||||||
docker compose -p "penpotdev-${instance}" \
|
docker compose -p "penpotdev-${instance}" \
|
||||||
--env-file "$DEVENV_DEFAULTS_FILE" \
|
--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"
|
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
|
# 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
|
# 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 serena_context="desktop-app"
|
||||||
local git_user_name=""
|
local git_user_name=""
|
||||||
local git_user_email=""
|
local git_user_email=""
|
||||||
|
local opencode_config_dir=""
|
||||||
local -a extra_env_args=()
|
local -a extra_env_args=()
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
@ -778,6 +813,8 @@ function run-devenv {
|
|||||||
do_attach=true; shift;;
|
do_attach=true; shift;;
|
||||||
--serena-context)
|
--serena-context)
|
||||||
serena_context="$2"; shift 2;;
|
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)
|
||||||
git_user_name="$2"; shift 2;;
|
git_user_name="$2"; shift 2;;
|
||||||
--git-user-email)
|
--git-user-email)
|
||||||
@ -787,13 +824,16 @@ function run-devenv {
|
|||||||
-e*)
|
-e*)
|
||||||
extra_env_args+=(-e "${1#-e}"); shift;;
|
extra_env_args+=(-e "${1#-e}"); shift;;
|
||||||
-h|--help)
|
-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 " Bring a single workspace up."
|
||||||
echo " --ws N target workspace (default: 0)."
|
echo " --ws N target workspace (default: 0)."
|
||||||
echo " --sync re-seed the wsN clone from the live repo (forbidden on ws0)."
|
echo " --sync re-seed the wsN clone from the live repo (forbidden on ws0)."
|
||||||
echo " --attach attach to the tmux session after startup."
|
echo " --attach attach to the tmux session after startup."
|
||||||
echo " --agentic enable MCP + Serena (AI-agent mode)."
|
echo " --agentic enable MCP + Serena (AI-agent mode)."
|
||||||
echo " --serena-context CTX context passed to Serena (default: desktop-app)."
|
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-name NAME git author name inside the container (default: host git config)."
|
||||||
echo " --git-user-email EMAIL git author email inside the container."
|
echo " --git-user-email EMAIL git author email inside the container."
|
||||||
echo " -e KEY=VAL forward env var to docker exec on attach."
|
echo " -e KEY=VAL forward env var to docker exec on attach."
|
||||||
@ -863,6 +903,14 @@ function run-devenv {
|
|||||||
write-instance-mcp-configs "$target"
|
write-instance-mcp-configs "$target"
|
||||||
fi
|
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..."
|
echo "Starting $target..."
|
||||||
start-instance "$target" "$serena_context" "$git_user_name" "$git_user_email" "$agentic"
|
start-instance "$target" "$serena_context" "$git_user_name" "$git_user_email" "$agentic"
|
||||||
print-instance-info "$target"
|
print-instance-info "$target"
|
||||||
@ -1338,6 +1386,11 @@ function usage {
|
|||||||
echo " --attach attach to the tmux session after startup."
|
echo " --attach attach to the tmux session after startup."
|
||||||
echo " --agentic enable MCP + Serena (AI-agent mode)."
|
echo " --agentic enable MCP + Serena (AI-agent mode)."
|
||||||
echo " --serena-context CTX passed to Serena (default: desktop-app)."
|
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 " -e KEY=VAL forwarded to 'docker exec' on attach."
|
||||||
echo " --git-user-name NAME / --git-user-email EMAIL"
|
echo " --git-user-name NAME / --git-user-email EMAIL"
|
||||||
echo " identity wired into the container's git config"
|
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_SERVER_PORT` | Port for the HTTP/SSE server | `4401` |
|
||||||
| `PENPOT_MCP_WEBSOCKET_PORT` | Port for the WebSocket server (plugin connection) | `4402` |
|
| `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_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_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_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` |
|
| `PENPOT_MCP_TOOL_TIMEOUT_S` | Timeout, in seconds, for tool calls dispatched to the Penpot plugin | `120` |
|
||||||
|
|||||||
@ -23,9 +23,9 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/penpot/penpot.git"
|
"url": "https://github.com/penpot/penpot.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^10.0.4",
|
"concurrently": "^10.0.5",
|
||||||
"prettier": "^3.9.6"
|
"prettier": "^3.9.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
"description": "Shared type definitions and interfaces for Penpot MCP",
|
"description": "Shared type definitions and interfaces for Penpot MCP",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --build --clean && tsc --build",
|
"build": "tsc --build --clean && tsc --build",
|
||||||
"watch": "tsc --watch",
|
"watch": "tsc --watch",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { defineConfig, mergeConfig } from "vite";
|
import { defineConfig, mergeConfig } from "vite";
|
||||||
import baseConfig from "./vite.config";
|
import baseConfig from "./vite.config.ts";
|
||||||
|
|
||||||
export default mergeConfig(
|
export default mergeConfig(
|
||||||
baseConfig,
|
baseConfig,
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
],
|
],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"class-transformer": "^0.5.1",
|
"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;
|
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
|
* Returns a short, non-reversible fingerprint of a user token, suitable for
|
||||||
* correlating log lines without exposing the full credential.
|
* correlating log lines without exposing the full credential.
|
||||||
@ -83,7 +107,7 @@ export class PenpotMcpServer {
|
|||||||
public readonly configLoader: ConfigurationLoader;
|
public readonly configLoader: ConfigurationLoader;
|
||||||
private app: any;
|
private app: any;
|
||||||
public readonly pluginBridge: PluginBridge;
|
public readonly pluginBridge: PluginBridge;
|
||||||
private readonly replServer: ReplServer;
|
private readonly replServer: ReplServer | null;
|
||||||
private apiDocs: ApiDocs;
|
private apiDocs: ApiDocs;
|
||||||
private readonly penpotHighLevelOverview: string;
|
private readonly penpotHighLevelOverview: string;
|
||||||
private readonly connectionInstructions: string;
|
private readonly connectionInstructions: string;
|
||||||
@ -149,7 +173,12 @@ export class PenpotMcpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.pluginBridge = new PluginBridge(this, this.webSocketPort, toolTimeoutSecs, this.redisBridge);
|
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.
|
* additional developer tools such as ClojureScript expression evaluation are exposed.
|
||||||
*/
|
*/
|
||||||
public isDevEnv(): boolean {
|
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(`Legacy SSE endpoint: http://${this.host}:${this.port}/sse`);
|
||||||
this.logger.info(`WebSocket server URL: ws://${this.host}:${this.webSocketPort}`);
|
this.logger.info(`WebSocket server URL: ws://${this.host}:${this.webSocketPort}`);
|
||||||
|
|
||||||
// start the REPL server and session timeout checker
|
// start the REPL server (devenv only) and session timeout checker
|
||||||
await this.replServer.start();
|
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();
|
this.startSessionTimeoutChecker();
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
@ -438,8 +484,11 @@ export class PenpotMcpServer {
|
|||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
this.logger.info("Stopping Penpot MCP Server...");
|
this.logger.info("Stopping Penpot MCP Server...");
|
||||||
clearInterval(this.sessionTimeoutInterval);
|
clearInterval(this.sessionTimeoutInterval);
|
||||||
|
await this.pluginBridge.close();
|
||||||
await this.redisBridge?.close();
|
await this.redisBridge?.close();
|
||||||
await this.replServer.stop();
|
if (this.replServer) {
|
||||||
|
await this.replServer.stop();
|
||||||
|
}
|
||||||
this.logger.info("Penpot MCP Server stopped");
|
this.logger.info("Penpot MCP Server stopped");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -467,4 +467,16 @@ export class PluginBridge {
|
|||||||
task.rejectWithError(error instanceof Error ? error : new Error(String(error)));
|
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:
|
allowBuilds:
|
||||||
esbuild: true
|
esbuild: true
|
||||||
sharp: false
|
sharp: false
|
||||||
@ -19,9 +14,9 @@ minimumReleaseAgeExclude:
|
|||||||
- hono@4.12.34
|
- hono@4.12.34
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
- "./packages/common"
|
- "packages/common"
|
||||||
- "./packages/server"
|
- "packages/server"
|
||||||
- "./packages/plugin"
|
- "packages/plugin"
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
'@hono/node-server@<2.0.5': ^2.0.5
|
'@hono/node-server@<2.0.5': ^2.0.5
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
"fmt:check": "prettier --check src/ test/",
|
"fmt:check": "prettier --check src/ test/",
|
||||||
"clean": "rm -rf dist/"
|
"clean": "rm -rf dist/"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.18.0+sha512.33d83c77da82f49fba836925c6f1b841181ec3132b670639bd012f7075f5c7cf634c5f870147c19aae7478fac01df09d8892e880454896edd23ee9b33757563c",
|
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"multer": "^2.2.0",
|
"multer": "^2.2.0",
|
||||||
@ -22,19 +22,19 @@
|
|||||||
"pino": "^10.3.1",
|
"pino": "^10.3.1",
|
||||||
"pino-loki": "^3.0.0",
|
"pino-loki": "^3.0.0",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"sharp": "^0.35.3",
|
"sharp": "^0.35.4",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
"@types/node": "^26.1.2",
|
"@types/node": "^26.3.0",
|
||||||
"@types/supertest": "^7.2.1",
|
"@types/supertest": "^7.2.1",
|
||||||
"esbuild": "^0.28.1",
|
"esbuild": "^0.28.2",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"supertest": "^7.2.2",
|
"supertest": "^7.2.2",
|
||||||
"tsx": "^4.22.4",
|
"tsx": "^4.23.12",
|
||||||
"typescript": "^7.0.2",
|
"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 .",
|
"lint": "eslint .",
|
||||||
"test": "vitest"
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee"
|
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,8 +23,8 @@
|
|||||||
"playwright": "^1.62.1",
|
"playwright": "^1.62.1",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.9.6",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^8.2.0",
|
"vite": "^8.2.2",
|
||||||
"vite-live-preview": "^0.4.0"
|
"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