Skip to content

security: supply chain hardening (provenance, env gate, CODEOWNERS, dependabot, audit) - #3

Closed
yokoszn wants to merge 239 commits into
mainfrom
security/supply-chain-hardening
Closed

yokoszn wants to merge 239 commits into
mainfrom
security/supply-chain-hardening

Conversation

@yokoszn

@yokoszn yokoszn commented Apr 1, 2026

Copy link
Copy Markdown

Summary

Implements the maintainer-compromise / supply chain hardening recommendations:

  • npm provenance attestation--provenance added to publish-packages.sh; every published package is now cryptographically bound to the GHA run that produced it (verifiable at npmjs.com). snapshot.yml also gets id-token: write so canary releases carry attestation too.
  • Environment protection gaterelease.yml job now requires environment: production, forcing named-reviewer approval before publish runs. A single compromised account can no longer ship unilaterally.
  • Dependency audit in CIyarn npm audit --all --recursive --severity high runs in the test job after install, blocking PRs that introduce known-high CVEs.
  • CODEOWNERS.github/CODEOWNERS requires @open-mercato/maintainers review on workflows, release/publish scripts, Dockerfile, auth modules, and SECURITY.md itself.
  • Dependabot.github/dependabot.yml opens weekly PRs for npm and GitHub Actions updates; minor/patch batched, major flagged for manual review.
  • SECURITY.md — new "Supply Chain Security" section documents the guarantees above for downstream consumers.

Setup required after merge

  1. Create a production GitHub Environment at Settings → Environments → New environment → name it production → add Required reviewers (at least 2 maintainers).
  2. Create the @open-mercato/maintainers team at Settings → Teams and add the maintainer accounts.

Test plan

  • Verify yarn npm audit passes on main (no high CVEs currently)
  • Confirm snapshot release on a PR carries provenance (check npmjs.com attestation tab after next canary)
  • After creating the production environment, trigger a dry-run workflow_dispatch on release.yml to confirm the approval gate appears
  • Confirm CODEOWNERS auto-requests maintainers on a test PR touching .github/workflows/

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

This release contains internal CI/CD workflow configuration updates with no user-facing changes or new features.

@coderabbitai

coderabbitai Bot commented Apr 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 297 files, which is 147 over the limit of 150.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8aebab98-3b84-41b1-81fa-93955f381471

📥 Commits

Reviewing files that changed from the base of the PR and between 614c34a and 4f27ff8.

⛔ Files ignored due to path filters (3)
  • apps/docs/static/screenshots/open-mercato-attachment-partitions.png is excluded by !**/*.png
  • apps/docs/static/screenshots/open-mercato-attachments-upload.png is excluded by !**/*.png
  • apps/docs/static/screenshots/open-mercato-workflows-visual-editor.png is excluded by !**/*.png
📒 Files selected for processing (297)
  • .ai/analysis/generated-files-comparison.md
  • .ai/design-system-audit-2026-04-10.md
  • .ai/docs/ds-v0-usage-guide.md
  • .ai/lessons.md
  • .ai/qa/.gitignore
  • .ai/qa/AGENTS.md
  • .ai/qa/tests/.gitignore
  • .ai/qa/tests/integration/TC-INT-007.spec.ts
  • .ai/reports/ds-health-2026-04-11.txt
  • .ai/reports/ds-health-baseline-2026-04-11.txt
  • .ai/scripts/ds-health-check.sh
  • .ai/scripts/ds-migrate-colors.sh
  • .ai/scripts/ds-migrate-typography.sh
  • .ai/skills/README.md
  • .ai/skills/code-review/SKILL.md
  • .ai/skills/code-review/references/review-checklist.md
  • .ai/skills/create-agents-md/SKILL.md
  • .ai/skills/dev-container-maintenance/SKILL.md
  • .ai/skills/ds-guardian/SKILL.md
  • .ai/skills/ds-guardian/references/component-guide.md
  • .ai/skills/ds-guardian/references/page-templates.md
  • .ai/skills/ds-guardian/references/token-mapping.md
  • .ai/skills/ds-guardian/scripts/ds-health-check.sh
  • .ai/skills/ds-guardian/scripts/ds-migrate-colors.sh
  • .ai/skills/ds-guardian/scripts/ds-migrate-typography.sh
  • .ai/skills/fix-github-issue/SKILL.md
  • .ai/skills/implement-spec/SKILL.md
  • .ai/skills/integration-builder/SKILL.md
  • .ai/skills/merge-buddy/SKILL.md
  • .ai/skills/review-pr/SKILL.md
  • .ai/skills/review-prs/SKILL.md
  • .ai/skills/smart-test/SKILL.md
  • .ai/skills/smart-test/references/test-architecture.md
  • .ai/skills/smart-test/scripts/find_affected_integration_tests.py
  • .ai/specs/2026-03-02-ready-apps-framework.md
  • .ai/specs/2026-03-20-decentralize-module-registry-generator.md
  • .ai/specs/2026-04-02-empty-app-starter-presets.md
  • .ai/specs/2026-04-03-advanced-datatable-ux.md
  • .ai/specs/2026-04-03-customers-lead-funnel.md
  • .ai/specs/2026-04-06-module-registry-ast-codegen-ts-morph.md
  • .ai/specs/2026-04-08-portal-custom-domain-routing.md
  • .ai/specs/2026-04-11-inbox-ops-local-dev-fixes.md
  • .ai/specs/2026-04-11-unified-ai-tooling-and-subagents.md
  • .ai/specs/2026-04-12-product-variant-media-display.md
  • .ai/specs/2026-04-13-pr-label-workflow.md
  • .ai/specs/SPEC-052-2026-02-22-integration-test-coverage-quick-wins.md
  • .ai/specs/SPEC-070-2026-04-04-perspectives-views-panel.md
  • .ai/specs/SPEC-071-2026-04-06-seo-helper-validation-visibility.md
  • .ai/specs/SPEC-072-2026-04-06-crm-detail-pages-ux-enhancements.md
  • .ai/specs/analysis/ANALYSIS-2026-04-03-module-registry-generator-parity.md
  • .ai/specs/analysis/ANALYSIS-2026-04-11-unified-ai-tooling-and-subagents.md
  • .ai/specs/implemented/2026-04-02-dev-structural-regeneration-and-cold-start-optimization.md
  • .ai/specs/implemented/2026-04-03-backend-first-load-client-first-sidebar.md
  • .ai/specs/implemented/2026-04-07-suppress-notice-bars-ephemeral-integration-tests.md
  • .ai/specs/implemented/2026-04-11-eliminate-global-roles.md
  • .ai/specs/implemented/2026-04-12-custom-field-default-values.md
  • .ai/specs/implemented/2026-04-12-customers-people-nested-profile-update-contract.md
  • .ai/specs/implemented/2026-04-12-disabled-example-module-build-failure.md
  • .ai/specs/implemented/SPEC-046c-2026-02-28-example-module-umes-alignment-customer-tasks.md
  • .ai/specs/implemented/SPEC-050-2026-02-26-dev-container-setup.md
  • .ai/specs/implemented/SPEC-060-2026-03-04-customer-identity-portal-auth.md
  • .ai/specs/implemented/SPEC-2026-04-12-tag-filter-display-fix.md
  • .ai/specs/implemented/SPEC-2026-04-12-workflows-openapi-response-spec.md
  • .devcontainer/README.md
  • .devcontainer/devcontainer.json
  • .devcontainer/scripts/generate-compose-volumes.sh
  • .github/CODEOWNERS
  • .github/PULL_REQUEST_TEMPLATE.md
  • .github/dependabot.yml
  • .github/workflows/ci.yml
  • .github/workflows/qa-deploy.yml
  • .github/workflows/snapshot.yml
  • .gitignore
  • AGENTS.md
  • CONTRIBUTING.md
  • README.md
  • RELEASE_NOTES.md
  • apps/docs/docs/api/attachments.mdx
  • apps/docs/docs/appendix/troubleshooting.mdx
  • apps/docs/docs/architecture/cache-cli.mdx
  • apps/docs/docs/architecture/generators.mdx
  • apps/docs/docs/cli/configs-cache.mdx
  • apps/docs/docs/cli/db-generate.mdx
  • apps/docs/docs/cli/dev-ephemeral.mdx
  • apps/docs/docs/cli/overview.mdx
  • apps/docs/docs/customization/create-first-module.mdx
  • apps/docs/docs/customization/create-inventory-api.mdx
  • apps/docs/docs/customization/create-inventory-data.mdx
  • apps/docs/docs/customization/inventory-crud-forms.mdx
  • apps/docs/docs/customization/list-inventory.mdx
  • apps/docs/docs/customization/standalone-app.mdx
  • apps/docs/docs/framework/modules/checkout.mdx
  • apps/docs/docs/installation/prerequisites.mdx
  • apps/docs/docs/installation/setup.mdx
  • apps/docs/docs/tutorials/first-app.mdx
  • apps/docs/docs/user-guide/akeneo-pim.mdx
  • apps/docs/docs/user-guide/attachments.mdx
  • apps/docs/docs/user-guide/cache-management.mdx
  • apps/docs/docs/user-guide/encryption.mdx
  • apps/docs/docs/user-guide/products-and-sales-channels.mdx
  • apps/docs/docs/user-guide/resources-and-resource-types.mdx
  • apps/docs/docs/user-guide/teams-and-team-members.mdx
  • apps/docs/docs/user-guide/webhooks.mdx
  • apps/docs/docs/user-guide/workflows/creating-workflows.mdx
  • apps/docs/sidebars.ts
  • apps/mercato/.env.example
  • apps/mercato/next.config.ts
  • apps/mercato/package.json
  • apps/mercato/scripts/__tests__/dev-runtime-log-policy.test.mjs
  • apps/mercato/scripts/dev-runtime-log-policy.mjs
  • apps/mercato/scripts/dev.mjs
  • apps/mercato/src/__tests__/AiAssistantShellIntegration.test.tsx
  • apps/mercato/src/__tests__/api-authorization.test.ts
  • apps/mercato/src/__tests__/bootstrap.test.ts
  • apps/mercato/src/app/(backend)/backend/[...slug]/page.tsx
  • apps/mercato/src/app/(backend)/backend/__tests__/backend-require-features.test.tsx
  • apps/mercato/src/app/(backend)/backend/__tests__/layout-route-registry.test.tsx
  • apps/mercato/src/app/(backend)/backend/layout.tsx
  • apps/mercato/src/app/(frontend)/[...slug]/page.tsx
  • apps/mercato/src/app/__tests__/layout.test.tsx
  • apps/mercato/src/app/api/[...slug]/route.ts
  • apps/mercato/src/app/api/events/route.ts
  • apps/mercato/src/app/globals.css
  • apps/mercato/src/app/layout.tsx
  • apps/mercato/src/app/page.tsx
  • apps/mercato/src/bootstrap.ts
  • apps/mercato/src/components/AiAssistantShellIntegration.tsx
  • apps/mercato/src/components/AppProviders.tsx
  • apps/mercato/src/components/BackendHeaderChrome.tsx
  • apps/mercato/src/components/OrganizationSwitcher.tsx
  • apps/mercato/src/components/__tests__/AppProviders.test.tsx
  • apps/mercato/src/i18n/de.json
  • apps/mercato/src/i18n/en.json
  • apps/mercato/src/i18n/es.json
  • apps/mercato/src/i18n/pl.json
  • apps/mercato/src/instrumentation.ts
  • apps/mercato/src/modules.ts
  • apps/mercato/src/modules/example/__integration__/TC-UMES-011.spec.ts
  • apps/mercato/src/modules/example/__integration__/TC-UMES-013.spec.ts
  • apps/mercato/src/modules/example/__integration__/TC-UMES-020.spec.ts
  • apps/mercato/src/modules/example/__integration__/TC-UMES-021.spec.ts
  • apps/mercato/src/modules/example/api/customer-priorities/route.ts
  • apps/mercato/src/modules/example/api/todos/route.ts
  • apps/mercato/src/modules/example/backend/customer-tasks/page.meta.ts
  • apps/mercato/src/modules/example/backend/customer-tasks/page.tsx
  • apps/mercato/src/modules/example/backend/payments/page.tsx
  • apps/mercato/src/modules/example/backend/todos/[id]/edit/page.tsx
  • apps/mercato/src/modules/example/backend/todos/create/page.tsx
  • apps/mercato/src/modules/example/cli.ts
  • apps/mercato/src/modules/example/commands/__tests__/todos.update.test.ts
  • apps/mercato/src/modules/example/commands/todos.ts
  • apps/mercato/src/modules/example/lib/__tests__/mock-gateway-adapter.test.ts
  • apps/mercato/src/modules/example/lib/mock-gateway-adapter.ts
  • apps/mercato/src/modules/example_customers_sync/acl.ts
  • apps/mercato/src/modules/example_customers_sync/api/example-customers-sync/mappings/route.ts
  • apps/mercato/src/modules/example_customers_sync/api/example-customers-sync/reconcile/route.ts
  • apps/mercato/src/modules/example_customers_sync/data/enrichers.ts
  • apps/mercato/src/modules/example_customers_sync/data/entities.ts
  • apps/mercato/src/modules/example_customers_sync/data/validators.ts
  • apps/mercato/src/modules/example_customers_sync/events.ts
  • apps/mercato/src/modules/example_customers_sync/i18n/de.json
  • apps/mercato/src/modules/example_customers_sync/i18n/en.json
  • apps/mercato/src/modules/example_customers_sync/i18n/es.json
  • apps/mercato/src/modules/example_customers_sync/i18n/pl.json
  • apps/mercato/src/modules/example_customers_sync/index.ts
  • apps/mercato/src/modules/example_customers_sync/lib/__tests__/mappings.test.ts
  • apps/mercato/src/modules/example_customers_sync/lib/__tests__/sync.test.ts
  • apps/mercato/src/modules/example_customers_sync/lib/inbound-subscriber.ts
  • apps/mercato/src/modules/example_customers_sync/lib/mappings.ts
  • apps/mercato/src/modules/example_customers_sync/lib/outbound-subscriber.ts
  • apps/mercato/src/modules/example_customers_sync/lib/queue.ts
  • apps/mercato/src/modules/example_customers_sync/lib/runtime.ts
  • apps/mercato/src/modules/example_customers_sync/lib/sync.ts
  • apps/mercato/src/modules/example_customers_sync/lib/toggles.ts
  • apps/mercato/src/modules/example_customers_sync/migrations/.snapshot-open-mercato.json
  • apps/mercato/src/modules/example_customers_sync/migrations/Migration20260401173723.ts
  • apps/mercato/src/modules/example_customers_sync/migrations/Migration20260408162620.ts
  • apps/mercato/src/modules/example_customers_sync/setup.ts
  • apps/mercato/src/modules/example_customers_sync/subscribers/customers-interaction-canceled.ts
  • apps/mercato/src/modules/example_customers_sync/subscribers/customers-interaction-completed.ts
  • apps/mercato/src/modules/example_customers_sync/subscribers/customers-interaction-created.ts
  • apps/mercato/src/modules/example_customers_sync/subscribers/customers-interaction-deleted.ts
  • apps/mercato/src/modules/example_customers_sync/subscribers/customers-interaction-updated.ts
  • apps/mercato/src/modules/example_customers_sync/subscribers/example-todo-created.ts
  • apps/mercato/src/modules/example_customers_sync/subscribers/example-todo-deleted.ts
  • apps/mercato/src/modules/example_customers_sync/subscribers/example-todo-updated.ts
  • apps/mercato/src/modules/example_customers_sync/workers/inbound.ts
  • apps/mercato/src/modules/example_customers_sync/workers/outbound.ts
  • apps/mercato/src/modules/example_customers_sync/workers/reconcile.ts
  • apps/mercato/tsconfig.json
  • docker-compose.fullapp.dev.yml
  • docker/README.md
  • docs/design-system/README.md
  • docs/design-system/audit.md
  • docs/design-system/champions.md
  • docs/design-system/component-apis.md
  • docs/design-system/component-specs.md
  • docs/design-system/components.md
  • docs/design-system/content-patterns.md
  • docs/design-system/contributor-experience.md
  • docs/design-system/contributor-guardrails.md
  • docs/design-system/coverage-report.md
  • docs/design-system/decision-log.md
  • docs/design-system/deliverables.md
  • docs/design-system/enforcement.md
  • docs/design-system/executive-summary.md
  • docs/design-system/foundations-gaps.md
  • docs/design-system/foundations.md
  • docs/design-system/hackathon-plan.md
  • docs/design-system/iteration.md
  • docs/design-system/lint-rules.md
  • docs/design-system/metrics.md
  • docs/design-system/migration-tables.md
  • docs/design-system/onboarding-guide.md
  • docs/design-system/principles.md
  • docs/design-system/priority-table.md
  • docs/design-system/research-plan.md
  • docs/design-system/risk-analysis.md
  • docs/design-system/stakeholder-buyin.md
  • docs/design-system/success-metrics-cx.md
  • docs/design-system/testing-designer.md
  • docs/design-system/token-values.md
  • eslint.config.mjs
  • jest.config.cjs
  • package.json
  • packages/ai-assistant/AGENTS.md
  • packages/ai-assistant/README.md
  • packages/ai-assistant/package.json
  • packages/ai-assistant/src/frontend/components/DockableChat/DockableChat.tsx
  • packages/ai-assistant/src/frontend/hooks/useCommandPalette.ts
  • packages/ai-assistant/src/frontend/hooks/useMcpTools.ts
  • packages/ai-assistant/src/modules/ai_assistant/api/chat/route.ts
  • packages/ai-assistant/src/modules/ai_assistant/api/health/route.ts
  • packages/ai-assistant/src/modules/ai_assistant/api/route/route.ts
  • packages/ai-assistant/src/modules/ai_assistant/api/settings/route.ts
  • packages/ai-assistant/src/modules/ai_assistant/api/tools/execute/route.ts
  • packages/ai-assistant/src/modules/ai_assistant/api/tools/route.ts
  • packages/ai-assistant/src/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.tsx
  • packages/ai-assistant/src/modules/ai_assistant/lib/__tests__/codemode-tools.test.ts
  • packages/ai-assistant/src/modules/ai_assistant/lib/__tests__/sandbox.test.ts
  • packages/ai-assistant/src/modules/ai_assistant/lib/api-endpoint-index.ts
  • packages/ai-assistant/src/modules/ai_assistant/lib/codemode-tools.ts
  • packages/ai-assistant/src/modules/ai_assistant/lib/opencode-client.ts
  • packages/ai-assistant/src/modules/ai_assistant/lib/sandbox.ts
  • packages/checkout/AGENTS.md
  • packages/checkout/README.md
  • packages/checkout/build.mjs
  • packages/checkout/src/modules/checkout/__integration__/helpers/fixtures.ts
  • packages/checkout/src/modules/checkout/__tests__/encryption.test.ts
  • packages/checkout/src/modules/checkout/api/pay/[slug]/submit/__tests__/route.test.ts
  • packages/checkout/src/modules/checkout/api/pay/[slug]/submit/route.ts
  • packages/checkout/src/modules/checkout/encryption.ts
  • packages/checkout/src/modules/checkout/index.ts
  • packages/cli/AGENTS.md
  • packages/cli/build.mjs
  • packages/cli/package.json
  • packages/cli/src/__tests__/mercato.test.ts
  • packages/cli/src/bin.ts
  • packages/cli/src/lib/__integration__/TC-INT-008.spec.ts
  • packages/cli/src/lib/__tests__/agentic-init.test.ts
  • packages/cli/src/lib/__tests__/modules-config.test.ts
  • packages/cli/src/lib/__tests__/next-build-id.test.ts
  • packages/cli/src/lib/__tests__/resolve-environment.test.ts
  • packages/cli/src/lib/__tests__/resolver.enterprise.test.ts
  • packages/cli/src/lib/__tests__/server-start-lock.test.ts
  • packages/cli/src/lib/__tests__/spawn.test.ts
  • packages/cli/src/lib/__tests__/umes-check.test.ts
  • packages/cli/src/lib/__tests__/utils.test.ts
  • packages/cli/src/lib/agentic-init.ts
  • packages/cli/src/lib/agentic-setup.ts
  • packages/cli/src/lib/db/__tests__/commands.test.ts
  • packages/cli/src/lib/db/commands.ts
  • packages/cli/src/lib/generators/__tests__/disabled-example-module.test.ts
  • packages/cli/src/lib/generators/__tests__/entity-ids.test.ts
  • packages/cli/src/lib/generators/__tests__/generators.test.ts
  • packages/cli/src/lib/generators/__tests__/interceptors-extension.test.ts
  • packages/cli/src/lib/generators/__tests__/module-entities.test.ts
  • packages/cli/src/lib/generators/__tests__/module-subset.test.ts
  • packages/cli/src/lib/generators/__tests__/openapi.test.ts
  • packages/cli/src/lib/generators/__tests__/output-snapshots.test.ts
  • packages/cli/src/lib/generators/__tests__/scanner.test.ts
  • packages/cli/src/lib/generators/__tests__/structural-contracts.test.ts
  • packages/cli/src/lib/generators/ast/imports.ts
  • packages/cli/src/lib/generators/ast/index.ts
  • packages/cli/src/lib/generators/ast/source-file.ts
  • packages/cli/src/lib/generators/ast/writers.ts
  • packages/cli/src/lib/generators/entity-ids.ts
  • packages/cli/src/lib/generators/extension.ts
  • packages/cli/src/lib/generators/extensions/ai-tools.ts
  • packages/cli/src/lib/generators/extensions/analytics.ts
  • packages/cli/src/lib/generators/extensions/command-interceptors.ts
  • packages/cli/src/lib/generators/extensions/component-overrides.ts
  • packages/cli/src/lib/generators/extensions/dashboard-widgets.ts
  • packages/cli/src/lib/generators/extensions/enrichers.ts
  • packages/cli/src/lib/generators/extensions/events.ts
  • packages/cli/src/lib/generators/extensions/guards.ts
  • packages/cli/src/lib/generators/extensions/inbox-actions.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The YARN_ENABLE_IMMUTABLE_INSTALLS environment variable was removed from yarn install steps in the standalone-integration job of the snapshot CI workflow. The yarn commands continue to execute without this step-level environment configuration.

Changes

Cohort / File(s) Summary
CI Workflow Configuration
.github/workflows/snapshot.yml
Removed YARN_ENABLE_IMMUTABLE_INSTALLS environment variable from yarn install and yarn add steps in the standalone-integration job (4-line deletion).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 A variable once stood tall in the CI flow,
But today it vanishes, off it does go,
Yarn runs freely, no restraints in sight,
The pipeline hops forward, ever more light! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The PR title mentions supply chain hardening measures, but the raw summary shows only a removal of YARN_ENABLE_IMMUTABLE_INSTALLS from the snapshot workflow, which contradicts the comprehensive security changes described in objectives. Verify if this PR contains all described changes (provenance, env gate, CODEOWNERS, dependabot, audit) or if the title overstates the actual changeset. The raw summary appears incomplete.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/supply-chain-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
.github/CODEOWNERS (1)

24-26: Consider protecting Dependabot config with CODEOWNERS as well.

Since dependency update policy is part of your supply-chain control plane, adding ownership for .github/dependabot.yml would close a governance gap.

Suggested addition
 # Security policy and tooling
 SECURITY.md                            `@open-mercato/maintainers`
+.github/dependabot.yml                 `@open-mercato/maintainers`
 .github/CODEOWNERS                     `@open-mercato/maintainers`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/CODEOWNERS around lines 24 - 26, Add ownership for the Dependabot
configuration by updating the CODEOWNERS entries to include the
.github/dependabot.yml file and assign it to the same owner group used for
security and tooling (e.g., `@open-mercato/maintainers`); locate the existing
entries for SECURITY.md and .github/CODEOWNERS and add a new line for
.github/dependabot.yml mapped to that owner so the dependabot config is
protected by the same reviewers/approvers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/dependabot.yml:
- Around line 17-20: The current dependabot ignore rule (the block with "ignore"
referencing dependency-name: \"*\" and update-types:
[\"version-update:semver-major\"]) suppresses all semver-major PRs; remove or
replace that ignore block and instead configure a Dependabot grouping or
specific rule that surfaces semver-major updates as PRs for manual review (e.g.,
add a groups or package-ecosystem rule that matches major updates and opens PRs
rather than ignoring update-types: [\"version-update:semver-major\"]).

In @.github/workflows/snapshot.yml:
- Around line 9-13: Remove the workflow-level id-token: write permission and
instead add id-token: write to only the snapshot job; specifically, in the
top-level permissions block keep contents: read and pull-requests: write but
delete id-token: write, then in the snapshot job (job name snapshot) add a
job-level permissions block containing id-token: write (and any other needed
perms), leaving the standalone-integration job without id-token permissions so
it runs with only the workflow-level minimal permissions.

---

Nitpick comments:
In @.github/CODEOWNERS:
- Around line 24-26: Add ownership for the Dependabot configuration by updating
the CODEOWNERS entries to include the .github/dependabot.yml file and assign it
to the same owner group used for security and tooling (e.g.,
`@open-mercato/maintainers`); locate the existing entries for SECURITY.md and
.github/CODEOWNERS and add a new line for .github/dependabot.yml mapped to that
owner so the dependabot config is protected by the same reviewers/approvers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8fea734-59fe-431f-8b0c-0e9595a45544

📥 Commits

Reviewing files that changed from the base of the PR and between 58e7eb5 and 8a15a1c.

📒 Files selected for processing (7)
  • .github/CODEOWNERS
  • .github/dependabot.yml
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • .github/workflows/snapshot.yml
  • SECURITY.md
  • scripts/publish-packages.sh

Comment thread .github/dependabot.yml Outdated
Comment thread .github/workflows/snapshot.yml
dominikpalatynski and others added 28 commits April 1, 2026 15:27
…vel guides (open-mercato#1151)

* feat(create-app): add implement-spec and integration-tests skills for standalone apps

Add two new agentic skills adapted for standalone app development:

- implement-spec: spec-driven implementation workflow with phase tracking,
  subagent coordination, unit/integration tests, and code-review gates.
  Removes core-only concerns (backward compatibility, extension mode decision).

- integration-tests: Playwright test authoring and execution workflow with
  app exploration, fixture management, failure analysis, and module gating.
  Replaces ephemeral environment with dev server approach.

Also scaffolds a Playwright config (.ai/qa/playwright.config.ts) using the
framework's test discovery from @open-mercato/cli, with GitHub Actions
reporter support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(agentic): add navigation guide, module-level standalone guides, and build pipeline extension

Navigation improvements for standalone apps:
- Add navigation-patterns.md reference (page.meta.ts fields, sidebar
  grouping, settings pages, anti-patterns)
- Fix incomplete page.meta.ts templates in module-scaffold skill
  (add pageGroup, pageGroupKey, pageOrder, breadcrumb, requireFeatures)
- Add navigation checks to code-review checklist and module-scaffold rules
- Add cross-reference in backend-ui-design skill

Module-level standalone guide support:
- Extend build.mjs to discover guides at
  packages/*/src/modules/*/agentic/standalone-guide.md
  (output as {pkg}.{mod}.md alongside existing package-level guides)
- Create standalone guides for all 9 core modules: customers, workflows,
  catalog, sales, auth, currencies, integrations, data_sync,
  customer_accounts
- Add Module-Specific Guides section to AGENTS.md.template task router

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* spec(perspectives): add SPEC-070 views panel redesign

* spec(perspectives): add SPEC-070 content

* spec(perspectives): update deps - dnd-kit from PR open-mercato#1144

---------

Co-authored-by: Piotr Karwatka <piotr@catchthetornado.com>
* advanced datatable

* integration tests

* fix integration tests (change of search box)

* one more pass of integration tests adjustments

* adjust for new search approach
* feat: layout optimization spec

* fix: gaps filled

* feat: hydrate backend chrome on the client

* fix: build fix

* fix: fix

* fix: CR fixes

* fix: integration test fix

* fix: integration tests stabilzied
…ato#1173)

Comprehensive spec for mapping custom domains (e.g., shop.acme.com) to
tenant portals with enterprise-grade cache architecture, guided UX, and
full platform integration (makeCrudRoute, mutation guards, response
enrichers, @open-mercato/cache, notification system, widget injection,
UMES handles).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…mercato#1141)

* feat: spec PoC

* Optimize dev route manifests and generator watch flow

* Add dev route warmup and lazy handler registries

* Fix generator code sanitization for lazy imports

* Add dev command output experience spec

* Fix dev/build optimization regressions

* Fix verification gate regressions

* fix: migration test filename fixes

* fix: cache and CI fixes

* fix: fixes

* feat: console logs getting nicer

* feat: waiting screen for the dev mode

* fix: dev splash fixes

* fix: fixes

* feat: standalone dev splash

* fix: fixes

* fix: progress bar fixes

* fix: set of fixes

* fix: CR fixes

* fix: fixes

* fix: build fix

* Fix standalone module registry discovery

* Fix standalone source-mirror CLI resolution

* fix: integration test stabilization

* fix: generator fix for missing workers

* fix: cache bustling for structural changes

* fix: translation loader fix

* fix: fixes

* fix: splash progress fix

* fix: metadata literal parser

* feat: module-registry refactor

* feat: unit tests prepared for modules-generated refactor

* feat: docker support for dev splash, readme update, spcs updateds for modules-registry refactor

* feat: spec update

* fix: dependabot fixes

* fix: merge issue fixes

* fix: switching nav to slim registry

* fix: typecheck fix

* fix: specs moved to `implemented`

* feat: classic mode

* optimization: module registry AST parity fixes on top of open-mercato#1141 (open-mercato#1165)

* feat: initial module-registry refactor

* feat: fixes in the AST driven code generators

* fix: compatibility analysis + legacy outputs

* fix unit tests fixed

* fix: generator castType fixed

* fix: unit tests extension

* fix: itnegration test fix

* fix: integration tests stabilization

* fix: workaround for TC-ADMIN-008
* add spec file

* update spec and implement solution

* add regression tests

* moved spec to implemented

---------

Co-authored-by: Piotr Karwatka <piotr@catchthetornado.com>
…rcato#1146)

- Fix checkout build: clear dist/ contents instead of rmSync on the
  directory itself (Docker named volume mount points can't be removed)
- Add docker-compose.local.yml for personal overrides (sibling repo
  mounts, extra env vars, additional services) — gitignored, auto-
  created with empty services mapping by generate-compose-volumes.sh
- Update README, SPEC-050 changelog, and skill file map

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* chore(deps): bump vulnerable transitive resolutions

* feat: module presets + empty starter
* fix: fixes

* fix: better SIGINT handling
…n-mercato#1175)

* fix: cusotmers search fixes, non-critical errors handling fix

* feat: init repo flow + ai coding flow

* fix: docs+ minor fixes

* fix: ui package export fix

* fix: template sync

* fix: missing dependency for test:create-app

* Potential fix for pull request finding 'CodeQL / Shell command built from environment values'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Potential fix for pull request finding 'CodeQL / Shell command built from environment values'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* feat: log dumping

* feat: repo create flow improvements

* fix: updated .gitignore

* fix: UI fixes, unit tests and logging fixes

* feat: github glow as a overlay

* fix: github button enabled/disabled

* fix: create repo button

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…pen-mercato#1070)

* feat(auth): invite users via email instead of admin-set passwords

* fix: sort auth module i18n translation keys

* fix(auth): harden invite flow error handling and UX issues

* fix(auth): resolve invite flow QA issues

- Remove in-app notification creation from invite and resend-invite flows to prevent duplicate email delivery via notification subscriber
- Add required marker to organization field on user edit page
- Show 'Resend Invite' button for all users, not just passwordless ones

* fix(auth): address CR findings — tenant scoping, mutation guard, tests, cleanup
pkarw and others added 18 commits April 14, 2026 11:04
…uard (open-mercato#1476)

* fix(auth/nav): reset attacker-controlled scope params on resolution failure and add auth.view feature guard

* Retrigger CI

* fix(auth): declare auth.view feature and grant to all roles, fix jest config

- Add auth.view feature to acl.ts so requireFeatures guard on admin nav
  endpoint resolves correctly for all authenticated users
- Grant auth.view to employee role in setup.ts defaultRoleFeatures to
  prevent locking non-admin users out of sidebar navigation
- Add @open-mercato/ui moduleNameMapper to jest.config.cjs for
  backendChrome.tsx dependency introduced by develop merge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* revert(auth): drop auth.view feature gate on admin nav

The auth.view feature guard added in this PR risked locking out
existing employee/non-admin users on systems where defaultRoleFeatures
have already been seeded (the new feature would not be granted).

Revert nav metadata to requireAuth: true only — every authenticated
user can call /api/auth/admin/nav as before. The downstream
resolveBackendChromePayload still applies per-route RBAC checks via
rbacService.userHasAllFeatures, so item-level access control is
unchanged.

The security fix from this PR is preserved:
- nav.ts catch block still resets selectedOrganizationId/Tenant to
  the authenticated user's values when resolveFeatureCheckContext
  throws (prevents attacker-supplied query params from being
  forwarded to the chrome payload)
- All three security regression tests are kept

Removed:
- auth.view entry in acl.ts
- requireFeatures: ['auth.view'] in nav.ts metadata
- superadmin and employee defaults in setup.ts (back to admin: ['auth.*'])
- "requires auth.view feature in GET metadata" test
- unused metadata import in test file

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Lukasz Stasko <lukasz.stasko@autopay.eu>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… (carry-forward open-mercato#1265) (open-mercato#1469)

* Added sanitize for rich text component

* Fix after code review

---------

Co-authored-by: Adam Kanigowski <adam.kanigowski@300.codes>
* feat(auth): enforce tenantId requirement for roles

- Updated CreateRoleFormValues to make tenantId optional and changed its default value to undefined.
- Modified CLI and command files to remove handling of global roles (tenantId: null) and enforce tenantId presence.
- Changed Role entity to make tenantId non-nullable.
- Updated migration to remove orphaned global roles and enforce NOT NULL constraint on tenantId.
- Adjusted various queries and logic to align with the new tenantId requirements.

* add tests and adjust fixture

* move spec to implemented

* fix

* fix merge

* fix(auth): address code review findings in tenant-scoped roles PR

Fix spec path in RELEASE_NOTES, remove dead null guards in undo handlers,
narrow EnsureRolesOptions.tenantId type, and clean up null-check readability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(auth): replace raw em.findOne/em.find with encryption helpers

Replace all raw em.findOne() and em.find() calls with
findOneWithDecryption() and findWithDecryption() per project
conventions. Tenant-scoped entities (Role, User, RoleAcl, UserAcl,
UserRole) must always use encryption helpers.

- commands/roles.ts: 7 replacements (6 findOne + 1 find)
- commands/users.ts: 9 replacements (7 findOne + 2 find)
- api/roles/route.ts: 3 replacements (3 find)
- lib/setup-app.ts: 8 replacements (findOne calls)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(skills): enforce encryption helper rule as hard blocker in review/fix skills

The findOneWithDecryption/findWithDecryption rule was sometimes skipped
because it was listed as a soft checklist item or medium-severity
auto-detection. Strengthen enforcement across all three skills:

- code-review: checklist item now says "every hit is a blocker" with
  explicit grep instruction; security rule names both helpers and import
- review-pr: promote raw em.find/em.findOne from Medium to High
  auto-detection with explicit grep command
- fix-github-issue: add validation loop step 6 to grep changed files
  for raw em.find; self-review checklist now includes grep instruction

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(auth): fix invite test mock to handle duplicate email check

The findOneWithDecryption mock returned a truthy value for all calls,
but after the encryption helper migration the user-create command now
makes two findOneWithDecryption calls: (1) organization lookup and
(2) duplicate-email check. Use mockResolvedValueOnce to return the org
for the first call and null for the second.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Marynat <jatyon14@wp.pl>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…forward open-mercato#1307) (open-mercato#1464)

* hackon(HCK-0083): tests: add low-level coverage for crud.ts

* hackon(HCK-0083): tests: add low-level coverage for crud.ts

* hackon(HCK-0083): tests: add low-level coverage for crud.ts

* chore: resolve merge conflicts with develop

Merge develop's comprehensive organizationScope tests with PR's new
whitespace-trimming test for resolveOrganizationScopeForRequest.
Preserve all other PR changes (normalizeOrganizationId, crud.ts trim,
crud-factory scope mock, integration test).

---------

Co-authored-by: pawelleszczewicz <30691854+pawelleszczewicz@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…y-forward open-mercato#777) (open-mercato#1460)

Add formatDescription and description field to tag filter options so
tag descriptions display in the filter overlay. Also fix the useMemo
dependency array for filterDefs — customerOptions and
loadCustomerOptions were missing, causing stale closures.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…en-mercato#1463)

* feat(ui): redesign perspectives panel as Views with column chooser integration (T-FE-02)

- Refactor PerspectiveSidebar into composable components (ViewChip, ViewChipMenu, NewViewForm, ShareForm)
- Integrate ColumnChooserSection into Views sidebar (single panel replaces two)
- Replace footer Save/Discard with autosave + toast
- Add chip-based view switcher with inline rename/clone/share/delete
- Add split button [Views | name] in DataTable toolbar
- Replace checkboxes with toggle switches, add --brand-violet token
- Add Shown/Available column sections with DnD reorder and Hide all
- Fix rename duplicate constraint, clone shared role preservation
- Filter duplicate role/personal chips by name
- Panel fixed right, back arrow nav, OM sidebar-inspired spacing
- Remove standalone ColumnChooserPanel and Columns3 button from DataTable

* fix(test): update TC-CRM-032 selectors for Views sidebar column chooser

* fix(ui): align Views sidebar with design system rules

- Replace hardcoded oklch() colors in ViewChip active state with
  brand-violet token classes (bg-brand-violet/10, border-brand-violet/30,
  text-brand-violet)
- Replace hardcoded text-red-600 error color with text-status-error-text
- Replace hardcoded amber warning colors with Alert variant="warning"
- Replace hand-rolled perspectivesCheckboxClassName with Checkbox
  primitive from @open-mercato/ui/primitives/checkbox (removes text-[10px]
  arbitrary size violation)
- Replace catch (err: any) with catch (err: unknown) + instanceof checks
- Remove duplicate cn import in DataTable.tsx from merge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ui): address review findings in Views sidebar redesign

- Restore missing onClick handler on mobile toolbar button (dead button)
- Remove 4 unused i18n keys (footer.discard, form.visible, tabs.private, tabs.shared)
- Add empty-state guard in ShareForm when roles array is empty
- Remove spurious key={id} prop on ViewChip inner div
- Fix i18n fallback mismatch in ColumnChooserPanel ('Available' → 'Available columns')

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: zielivia <zielivia@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ato#1456)

* spec: streamlined PR label workflow and QA pipeline

Reduce 50+ labels to 15 with a clear state machine (review → changes-requested → qa → merge-queue). Integrates review-pr skill, human QA, and CI into a single coherent flow. Includes migration script and skill update plan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: specs extensions

* spec(pr-workflow): add in-progress concurrency lock + wire skills

Adds a Concurrency Control section to the PR label workflow spec describing
how auto-skills (review-pr, fix-github-issue, review-prs, merge-buddy) MUST
claim a PR/issue before mutating it and MUST refuse to clobber an existing
claim unless --force is passed (and the user explicitly confirms).

- Spec: new Concurrency Control section with claim/release protocol, three
  signals (assignee, in-progress label, claim comment), --force semantics,
  stale-lock recovery, batch-skill skip rules.
- Spec: keep `in-progress` label (16-label inventory, was 15) and remove it
  from the deletion list in the migration script.
- review-pr SKILL.md: new step 0 (pre-claim check + claim) and step 11
  (release lock with finally semantics); --force argument documented.
- fix-github-issue SKILL.md: same step 0 and end-of-step-11 release on the
  issue; assignee remains so the human owns the resulting PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…orward open-mercato#1368) (open-mercato#1453)

* fix(auth): reject deleted users during session token refresh

refreshFromSessionToken queries User by sess.user.id without filtering
by deletedAt: null. A soft-deleted user whose session token was captured
before deletion can still refresh to new JWTs indefinitely.

Fix: add deletedAt: null to the User lookup in refreshFromSessionToken.

Tests: 2 new regression cases (deleted user rejected, active user OK).

* fix(auth): address review — findOneWithDecryption, deletedAt on confirmPasswordReset and findUserByEmail

Per review from @pkarw and @MStaniaszek1998:
- refreshFromSessionToken: use findOneWithDecryption instead of em.findOne
- confirmPasswordReset (line 109): add deletedAt: null filter so deleted
  users cannot reset their password with a valid token
- findUserByEmail (line 13): add deletedAt: null filter for consistency
  with findUsersByEmail and findUserByEmailAndTenant

* fix(auth): use findOneWithDecryption consistently in all User lookups

findUserByEmail and confirmPasswordReset still used raw em.findOne
instead of findOneWithDecryption. This aligns them with the encryption
helper mandate and the fix already applied to refreshFromSessionToken.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: RMN-45 <253251827+RMN-45@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…de effects (open-mercato#1452)

* fix(sales,workflows): add pessimistic locking to prevent duplicate side effects

Fixes two TOCTOU race conditions that could cause duplicate side effects:

1. Sales quote accept endpoint: parallel requests could both read status='sent',
   pass the status check, and invoke convert_to_order. Now the quote fetch + status
   check + update is wrapped in a transaction with PESSIMISTIC_WRITE lock.

2. Workflow resumeWorkflowAfterActivities: concurrent activity completions could
   both observe WAITING_FOR_ACTIVITIES status and run the resume flow twice. Now
   wrapped in a transaction with PESSIMISTIC_WRITE lock on the instance row.

Related to open-mercato#1339

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(shared): stabilize flaky interceptor timeout test

Increase the timeout gap in the custom-route-interceptor timeout test
from 5ms/20ms to 10ms/200ms. The tight 4x margin caused intermittent
CI failures under load; the new 20x margin is reliable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…es (carry-forward open-mercato#1286) (open-mercato#1461)

* fix(security): make JWTs revocable and isolate staff/customer audiences

Previously a stolen JWT remained valid until its exp — logout cleared the
cookie but did not invalidate the token server-side. Staff and customer
tokens also shared the same HS256 secret, so a staff cookie copied into
customer_auth_token passed verification (and vice versa).

Two defense-in-depth layers:

1. Audience-derived signing keys. The staff audience signs with
   HMAC(JWT_SECRET, "open-mercato:jwt:v1:staff") and the customer audience
   signs with the corresponding customer label. A token copied across
   audiences fails signature verification; on top of that, iss/aud claims
   are now enforced so even a mis-signed token with matching aud is rejected.
   Operators can still override a single audience via JWT_STAFF_SECRET /
   JWT_CUSTOMER_SECRET.

2. Session binding via sid. Every login (staff and customer) now creates
   a session row and embeds its id as the JWT sid claim. sessionIntegrity
   and customerAuth look up the session on every request and fail closed
   when the row is missing, soft-deleted, or expired. Logout extracts sid
   from auth_token and revokes the session by id, so stolen tokens stop
   working immediately instead of waiting for exp. Legacy tokens without
   sid are rejected so clients must re-authenticate after rollout.

SSO, MFA interceptors, MFA shared helpers, and the profile PUT route all
propagate sid through re-signed tokens so the session binding survives
every JWT refresh path.

Test coverage (all passing):
- jwt.test.ts: audience derivation, env override, normalization,
  cross-audience rejection, forged-aud rejection, legacy raw-secret path.
- sessionIntegrity.test.ts: legacy token without sid, revoked session,
  expired session, invalid sid UUID.
- authService.test.ts: deleteSessionById, findActiveSessionById happy/
  missing/expired paths.
- logout.test.ts: revoke by sid, revoke session_token, idempotent logout,
  invalid JWT, legacy token without sid.
- customerAuth.test.ts: revoked session rejection, missing sid, staff->
  customer replay rejection, fail-closed on DB error.
- customerSessionService.test.ts: sid in JWT, cross-audience rejection,
  findActiveSessionById variants.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(auth): resolve rebase conflicts and fix test mocks for session binding

- Update sessionIntegrity test to mock findOneWithDecryption for Session entity
  (replaces raw em.findOne after review fix)
- Add expiresAt to session mock objects for expiry validation
- Add sid to resolveCanonicalStaffAuthContext calls
- Add findOneWithDecryption mock to customerAuth test for isSessionRevoked check
- All 49 auth/customer_accounts tests pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(auth): ensure JWT_SECRET is available in CI unit tests

Three test files (customerAuth, logout, customerSessionService) call
signJwt/signAudienceJwt which now requires JWT_SECRET. Fix:

1. Add beforeAll fallback in each test file setting JWT_SECRET when
   missing — tests are self-contained regardless of env
2. Add JWT_SECRET env var to the CI test job (belt and suspenders)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(auth): add legacy JWT verification fallback and sync CustomerAuthContext

Three review findings addressed:

1. Legacy grace period: verifyJwt now falls back to raw JWT_SECRET when
   audience-derived verification fails, controlled by JWT_LEGACY_GRACE_MINUTES
   env (defaults to enabled). This prevents force-logout of all users on
   deploy and supports rolling deployments where old/new servers coexist.
   Set JWT_LEGACY_GRACE_MINUTES=0 for hard cutover.

2. Session integrity: legacy tokens (without sid) that pass the fallback
   verification are allowed through with _legacyToken flag, skipping
   session binding. They expire naturally via their exp claim.

3. CustomerAuthContext type sync: added sid field to the shared package
   type contract so portal UI hooks and third-party modules see the
   correct interface.

Test coverage:
- 4 new JWT legacy fallback tests (grace enabled, disabled, explicit
  secret, new tokens bypass)
- 1 new sessionIntegrity test (legacy token grace period)
- All 93 auth + 47 customer_accounts tests pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(sso): destructure { session, token } from createSession

The SSO completeLogin call site was missed when createSession was
refactored to return { session, token } in this PR. CI typecheck
caught it: ssoService.ts:135 used session.id where session was now
{ session, token }.

This mirrors the login.ts pattern: destructure both, use
session.id for the sid claim and the raw token for the
session_token cookie. The cookie must contain the raw token
(the DB stores the HMAC hash from develop's session-token hashing
change), so returning session.token (the hash) for sessionToken
would have broken refresh.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: WH173-P0NY <adas.kardasz@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
open-mercato#919) (open-mercato#1468)

* test: add regression test for quote-to-order document number generation (open-mercato#919)

Verifies that converting a quote to an order generates a new ORDER-prefixed
document number instead of copying the quote's QUOTE-prefixed number.

Covers the fix from PR open-mercato#1097 to prevent regression.

Closes open-mercato#919

* fix(sales): use findOneWithDecryption in quote-to-order conversion flow

Replace raw em.findOne/em.find calls with findOneWithDecryption/
findWithDecryption in the convert_to_order command handler and its
helpers (loadQuoteSnapshot, loadOrderSnapshot) to support tenant
data encryption maps.

Changes:
- loadQuoteSnapshot: findOneWithDecryption for SalesQuote,
  findWithDecryption for SalesQuoteLine + SalesQuoteAdjustment
- loadOrderSnapshot: findOneWithDecryption for SalesOrder,
  findWithDecryption for SalesOrderLine + SalesOrderAdjustment
- convert_to_order execute: findOneWithDecryption for SalesQuote
  (with PESSIMISTIC_WRITE lock preserved) and SalesOrder existence check
- convert_to_order undo: findOneWithDecryption for SalesOrder lookup
- Update both test files to wire mocked findOneWithDecryption/
  findWithDecryption instead of em.findOne/em.find

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(sales): update scope test mock to delegate findOneWithDecryption to em.findOne

The documents.scope.test.ts was failing because findOneWithDecryption
was mocked to always return null, but after the encryption helper
migration, loadQuoteSnapshot/loadOrderSnapshot now use
findOneWithDecryption for the main entity lookup. The mock now
delegates to em.findOne so the quote/order entity is found, allowing
the test to reach the SalesDocumentAddress query it actually validates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(sales): drop unnecessary as any from quote-to-order migration

The previous encryption-helper migration added `as any` to every new
findOneWithDecryption/findWithDecryption call, including the FilterQuery
arguments. They were not needed: existing call sites in the same module
(payments.ts, returns.ts) call the same helpers with typed FilterQuery
literals like { id: x }, { id, deletedAt: null }, and { quote } / { order }
without any cast. Aligning with that pattern.

Production code (documents.ts):
- loadQuoteSnapshot / loadOrderSnapshot: drop as any from { id, deletedAt: null }
  and { quote } / { order } FilterQuery literals
- convertQuoteToOrderCommand execute: drop as any from quote/existingOrder lookups
- convertQuoteToOrderCommand undo: drop as any from existingOrder lookup

Tests (documents.convert-to-order.test.ts, quoteConvertNumber.test.ts):
- replace `as any` on entity mock returns with `as unknown as <Entity>`
  so the entity type is preserved through the assertion path
- replace `ctx as any` with `as unknown as CommandRuntimeContext`
  (typed import added from @open-mercato/shared/lib/commands/types)

Pulled latest develop in for the in-progress integration-test fix
(session/token destructure from PR open-mercato#1461) so CI can revalidate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(sales): remove type castings from quote-to-order tests

Replace `as any`, `as jest.MockedFunction<typeof X>`, and `<any, any>`
generic placeholders introduced during the encryption-helper migration
with proper typed alternatives:

- `jest.mocked(X)` instead of `as jest.MockedFunction<typeof X>`
- `commandRegistry.get<ConvertToOrderInput, ConvertToOrderResult>` instead
  of `<any, any>`
- Real `awilix` container + `CommandRuntimeContext` typed ctx instead of
  `ctx as any` cast
- Property-narrowing helpers (`readWhereId`, `readLockOption`) using
  `'in' operator` checks for unknown mock arguments
- Structural `EmWithFindOne` and `QuoteStub` types in place of `em: any`

All 31 sales command tests still pass; full @open-mercato/core suite (2647
tests) is green and `tsc --noEmit` exits 0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: pawelleszczewicz <30691854+pawelleszczewicz@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ato#1240) (open-mercato#1454)

* fix: prevent catalog variant table overflow

* fix(catalog): use title attribute in variant table integration tests

Variant name and SKU cells now use CSS truncation with title tooltips.
Update TC-CAT-005 and TC-CAT-006 to assert on title attributes instead
of getByText, which fails when text is visually clipped by overflow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(catalog): stabilize variant integration tests for CSS truncation

TC-CAT-005/006 fail in CI because CSS `truncate` on variant name/SKU
cells makes elements visually hidden in narrow viewports. The elements
exist in the DOM with correct title attributes, but Playwright's
`toBeVisible()` rejects hidden elements.

Fixes:
- Use `toBeAttached()` instead of `toBeVisible()` for title-attribute
  assertions — verifies data persistence without viewport dependency
- TC-CAT-006: use title-attribute locator with force click instead of
  `getByRole('link', { name: 'Edit' })` which accidentally substring-
  matched "Editable Variant..." instead of an actual Edit action

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(catalog): navigate directly to variant edit URL in TC-CAT-006

The previous force-click on the truncated variant name link failed in CI
with "Element is outside of the viewport" — Playwright's `force: true`
still requires the click point to land inside the viewport, and the
table-fixed cell with `block truncate` puts the link in a clipped layout
that scrollIntoView cannot reliably bring fully into view.

Replace the brittle click with a direct goto after asserting the variant
is rendered. This still verifies that the variant appears in the product
detail page, then exercises the actual feature under test (variant SKU
edit + persistence) without depending on truncated-cell click behaviour.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(catalog): set min-w on variant table so name column stays usable

The earlier `table-fixed` + per-column widths fix kept the table from
overflowing horizontally, but on detail pages where the right-side
custom-fields panel (FIELDSET / IDENTITY / MATERIALS / CARE) takes ~33%
of the viewport, the variant column collapsed to ~30px — clipping the
title-link beyond what `truncate` could rescue and breaking click
targets in CI.

Add `min-w-[720px]` to the `<table>` so columns retain readable widths
in narrow containers. The wrapping `overflow-x-auto` already provides
horizontal scroll when the table cannot fit, so this prevents the
collapse without re-introducing the page-level overflow the original
fix prevents.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Michal Cygankiewicz <m.cygankiewicz@amtsolution.pl>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…arry-forward open-mercato#1485) (open-mercato#1488)

* fix: translations

* fix(i18n): sync missing translations + restore BC-critical exports (carry-forward open-mercato#1485)

Carry-forward of open-mercato#1485 (fork PR by @Sawarz) with the blocker findings
from code review applied so it can merge cleanly.

Applied fixes:
- Restored `export const emitCatalogEvent` in `packages/core/src/modules/catalog/events.ts`
  (removed in open-mercato#1485 — breaks BC surface #4 *Import paths: STABLE*).
- Restored `export const emitSalesEvent` in `packages/core/src/modules/sales/events.ts`
  (same reason).
- Restored `packages/core/src/modules/sales/lib/statusHistory.ts` with its
  `StatusChangeLogInput` type and `logStatusChange` helper (part of SPEC-006 / SPEC-059
  public surface).
- Added the two new `auth.acl.*` keys and fourteen new `workflows.checkoutDemo.*`
  keys to `pl.json`, `es.json`, and `de.json` so `i18n-check-sync` passes
  (this was the root cause of the `test` job failure on open-mercato#1485).
- Sorted the touched locale files alphabetically per the check's format rule.
- Kept the original author's AclEditor i18n hookups, workflows checkout-demo
  i18n hookups, and package.json `@types/*` devDeps move.

Credit: original translations + AclEditor wiring by @Sawarz in open-mercato#1485.

CI/verification:
- `yarn build:packages` — OK
- `yarn generate` — OK
- `yarn typecheck` — OK across all 18 packages
- `yarn test` — 2364/2364 pass
- `yarn tsx scripts/i18n-check-sync.ts` — clean

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Sawarz <sawarz22@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…t (carry-forward open-mercato#1483) (open-mercato#1486)

* fix(security): hash message access and quote acceptance tokens at rest (carry-forward open-mercato#1483)

Carry-forward of open-mercato#1483 scoped to the token types not already covered by
open-mercato#1465 (which hashed staff session + password-reset tokens on 2026-04-14).

Covers the remaining scope from open-mercato#1412:
- MessageAccessToken.token — now stored as HMAC-SHA256 hash; the raw
  token is still returned from createMessageAccessToken for use in the
  email URL.
- SalesQuote.acceptanceToken — hashed at write; raw UUID still embedded
  in the customer-facing /quote/<token> URL.

Pattern matches open-mercato#1465:
- Reuses the existing HMAC helper at
  packages/core/src/modules/auth/lib/tokenHash.ts (hashAuthToken,
  generateAuthToken) keyed by AUTH_TOKEN_SECRET / AUTH_SECRET /
  NEXTAUTH_SECRET / JWT_SECRET.
- Dual-lookup (hashed first, raw fallback) so in-flight emails and
  open quote-acceptance links continue to work for one release.
- No schema changes — column names stay token / acceptance_token, so
  the ADDITIVE-ONLY DB contract (root AGENTS.md) is preserved and no
  migration is needed.

Tests:
- messages/commands: new tokens.test.ts covers hashed lookup, legacy
  raw-token fallback, and not-found.
- messages/lib: email-sender.test.ts now asserts the persisted token
  is the HMAC hash, not the raw value.
- sales/api: quotes.acceptance.test.ts now asserts the stored
  acceptance_token is a 64-char hex hash, plus a new case exercising
  the raw-fallback lookup for pre-hash quotes.

All 2706 core unit tests pass.

Credit: @muhammadusman586 raised this scope in open-mercato#1483; the auth-side
work was already covered by open-mercato#1465 (carry-forward of @WH173-P0NY's
open-mercato#1277).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(security): pre-auth preflight for protected message tokens (carry-forward open-mercato#1260, open-mercato#1479)

Folds the `actionRequired=true` preflight from open-mercato#1260 / open-mercato#1479 into this
PR so the messages security work ships as one coherent change.

Until now, GET /api/messages/token/<token> consumed the access token
and returned the full Message payload (subject, body, actionData,
recipientUserId, object snapshots) before any authentication check.
For messages that carry MessageObject rows with `actionRequired=true`
this is a payload leak — the email recipient is the only intended
viewer, but anyone with the link could see the protected content.

This change:
- Splits token validation into resolveTokenAccess() so we can decide
  whether auth is required *before* consuming the token.
- For messages with any actionRequired object, returns
  `{ requiresAuth: true }` for unauthenticated requests and 403
  `{ error: 'Forbidden', requiresAuth: true }` for the wrong user;
  only consumes the token after the recipient is verified.
- Switches every read in the route from raw em.find/em.findOne to
  findOneWithDecryption / findWithDecryption with tenant scope derived
  from the loaded Message (per the encryption MUST rule). This was the
  main blocker on the previous review of open-mercato#1260.
- Resolves the access token via hashed-then-raw lookup so the
  preflight also works on the new HMAC-hashed message tokens
  introduced earlier in this PR.
- messageTokenResponseSchema becomes a discriminated union of
  detail + preflight responses, so the OpenAPI contract stays
  additive.
- Frontend page uses isProtectedPreflight() type guard before
  rendering the auth-required state.

Tests:
- 7/7 messages token route tests pass (preflight, 403, authenticated
  payload, error paths).
- All 118 affected tests across messages + sales pass.

Credit: original work by @WXYZx in open-mercato#1260 and folded forward by pkarw
in open-mercato#1479 (carry-forward of open-mercato#1263). This change supersedes both open-mercato#1260
(checkout/currency parts excluded — out of scope here) and open-mercato#1479
(separate-column SHA-256 approach replaced with HMAC same-column from
open-mercato#1465 to keep one consistent token-hashing pattern).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rry-forward open-mercato#1482) (open-mercato#1487)

* fix(workflows): use filterIds for org scoping in all GET handlers

When "All Organisations" is selected, scope.selectedId is null. The
workflow API routes were falling back to auth.orgId, returning only
the user's default org data. Now uses scope.filterIds (ACL-scoped)
to query across all permitted organisations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(workflows): align route tests with filterIds-based org scoping

The GET handlers in PR open-mercato#1482 switched from auth.orgId string scoping to
scope.filterIds (rendered as `organizationId: { \$in: [...] }`). Update the
unit-test assertions in definitions.route.test.ts and instances.route.test.ts
to match the new query shape so the suites pass.

Also add coverage for the two branches the fix exists to enable:
- multi-org scope (scope.filterIds.length > 1) emits $in with all orgs
- wildcard scope (scope.filterIds === null) omits the organizationId filter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(workflows-tests): extract shared org-scope assertion helpers

The multi-org and wildcard-scope tests were duplicated between
definitions.route.test.ts and instances.route.test.ts. Extract them into
orgScopeAssertions helpers so future callers (tasks, events, other
route handlers) can reuse the same assertions without copy-paste.

Each test body shrinks from ~15 lines of mock-setup + assertion to a
single helper call; the shape of the expected query ($in vs omitted
organizationId) is now described in one place.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(workflows): extract resolveOrganizationScopeFilter for route handlers

The 9 GET handlers in the workflows API each reimplemented the same
IIFE that translated an OrganizationScope + AuthContext into the
Mikro-ORM where-fragment ({ organizationId: { \$in: […] } }) or an
empty fragment for wildcard scope. That duplication was the real DRY
violation flagged in review (the prior commit only deduped the tests).

Extract a pure helper `resolveOrganizationScopeFilter` in
directory/utils/organizationScopeFilter.ts. It returns three fields:

- organizationIds — the resolved list (or undefined for wildcard)
- where — the Mikro-ORM fragment ready to spread into a where clause
- rbacOrganizationId — a single org id for RBAC checks that expect one

All 9 handlers now call the helper once and spread `orgFilter.where`
into their query. The events routes additionally use
`orgFilter.rbacOrganizationId` for the `rbacService.userHasAllFeatures`
call, replacing the inline `scope?.selectedId ?? auth.orgId` expression.

Placed in a sibling file rather than in organizationScope.ts so the
existing `jest.mock('.../organizationScope', () => ({ ... }))` mocks
in workflow route tests do not need to stub the new helper.

Added unit tests covering all 5 branches (selectedId, filterIds > 0,
wildcard filterIds === null, empty filterIds falling back to orgId,
both missing).

Net: -77 lines of duplicated logic removed across the 9 routes,
+28 line helper, +74 line unit test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Jacek Tomaszewski <jacek@fullstack.house>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* docs: clarify build order before initialize

* Improve Windows setup and bootstrap DX

* Refine Windows setup docs and DB pool defaults

* docs(windows): add native setup bootstrap script

* Fix CLI review issues and narrow Windows setup

* Trim Windows setup script post-run instructions

* Limit Windows setup script to native prerequisites

* Replace Windows setup script with manual docs

* Fix Windows spawn handling in dev runtimes
@yokoszn
yokoszn force-pushed the security/supply-chain-hardening branch 3 times, most recently from e7ba83f to 614c34a Compare April 14, 2026 15:30
…ot workflow

The standalone-integration job was setting YARN_ENABLE_IMMUTABLE_INSTALLS=0
to bypass lockfile enforcement when installing into the scaffolded app.
This is no longer needed — the scaffolded app ships its own lockfile
and yarn install runs cleanly without the override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yokoszn
yokoszn force-pushed the security/supply-chain-hardening branch from 614c34a to 4ea1246 Compare April 14, 2026 15:35
…CODEOWNERS

- snapshot.yml: move id-token: write from workflow-level permissions to the
  snapshot job only. The standalone-integration job no longer has the ability
  to mint OIDC tokens, closing the window where a compromised published package
  running in that job could request a valid token.

- dependabot.yml: add Dependabot config with minor/patch grouping to reduce
  noise and a separate major group so breaking-change bumps surface as their
  own PR for manual review rather than being silently suppressed.

- CODEOWNERS: require maintainer approval for CI/CD workflows, release scripts,
  auth modules, and security-sensitive paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yokoszn yokoszn closed this May 6, 2026
yokoszn pushed a commit that referenced this pull request Jun 16, 2026
…ocale (carry-forward of open-mercato#1730) (open-mercato#1781)

* feat(auth,ui): sidebar customization page with variants, DnD, cross-locale

Adds /backend/sidebar-customization with multi-variant CRUD, role apply,
DnD reorder of items inside groups (persisted itemOrder), cascade hide
for parent items, and a friendly add-new dialog. Variants and preferences
are now scoped per (user, tenant) — locale dropped from unique constraints
with a dedupe migration. Search input migrated to DS Input primitive,
section sidebar styling synced with main sidebar (active marker visible
again after removing inner overflow on the section scroll container).
Init effect drops the cancelled flag so React Strict Mode in dev cannot
abort the first init pass and leave the editor stuck on the loading
skeleton.

Adds 5 integration tests TC-AUTH-034..038 covering variant CRUD,
duplicate-name 409 with friendly error, soft-delete + recreate (partial
unique index regression guard), itemOrder round-trip, and add-new
dialog UI flow.

* refactor(ui): sticky sidebar with scroll affordance + settings search + DS layout polish

- Aside is now sticky (lg:sticky lg:top-0 lg:h-svh) with a hidden scrollbar
  (.scrollbar-hide utility) and a gradient-fade chevron at the bottom that
  flips 180 degrees when the user reaches the end of the scroll.
- Settings sidebar gets the DS Input search (mirrors main sidebar) with
  query-based item filtering; the redundant back-to-main link is removed.
- SectionPage moves padding from <aside> to the inner scroll container so
  the absolute active marker stays inside the padding-box (CSS clip happens
  at the padding-box edge, not outside it).
- SidebarCustomizationEditor wraps its content in <Page>/<PageBody> for
  consistency with other backend pages, replaces the role status text with
  DS Tags (info / error + AlertTriangle for "will clear preset"), and
  simplifies the init effect so React Strict Mode in dev no longer aborts
  the first init pass and leaves the editor stuck on the loading skeleton.

* chore(i18n): sync de/es translations for sidebar customization keys

Sidebar customization editor added new appShell.sidebarCustomization*
keys to en.json without backfilling de/es/pl. CI test job runs
i18n-check-sync which fails on missing keys. `yarn tsx
scripts/i18n-check-sync.ts --fix` backfills de/es with EN values as
placeholders (translation TODO), sorts en/pl. No runtime change.

* test(qa): scope getByRole textbox Search to exact match

Sidebar customization adds a settings search input with
aria-label='Search navigation'. Pre-existing tests used
getByRole('textbox', { name: 'Search' }) which matches by substring,
hitting both the sidebar input and the DataTable search input,
producing 'strict mode violation: 2 elements'.

Switch to { name: 'Search', exact: true } in the shared authUi helper
plus 8 spec files (TC-AUTH-010/011/012/013, TC-CAT-012,
TC-ADMIN-001/002/007, TC-INT-004) so the selector stays scoped to the
page-level search input that has aria-label exactly 'Search'.

* test(qa): extend TC-MSG-009 safeFill timeouts for CI shard load

The local safeFill helper used Playwright's default 5s expect timeout
and called keyboard.type immediately after click+focus+clear. Under
CI shard 9 parallel load the inline composer sometimes needs longer
than 5s to finish its mount/state-sync before the textarea is fully
ready; the typed characters then race the React commit and the value
assertion times out with Received "".

Defense in depth: assert toBeVisible + toBeEnabled before interacting
(10s each), extend toHaveValue to 60s, and bump the test-level timeout
to 180s so multiple safeFill chains plus waitForResponse fit. Same
pattern that stabilised TC-CRM-002 in open-mercato#1739.

Local: TC-MSG-009 passes in 7.9s (well under the 180s cap) with the
new timeouts.

* test(qa): use locator.fill + pre-submit reassert in TC-MSG-009

CI shard 9 trace (run 25095248488 retry #1):
- safeFill toHaveValue 60s timeout, 59 polls = textarea empty under
  parallel-shard load.
- Initial run safeFill passed but waitForResponse 180s timeout, page
  snapshot showed the inline reply textarea empty at 180s — controlled
  state had been silently dropped between safeFill (typed body) and the
  submit button click.

keyboard.type races the React state commit when MessageComposer mounts
inside MessageDetailPageClient and runs its own effects in parallel —
characters land but are immediately overwritten before the caller can
proceed. locator.fill is atomic (`element.value = …` + dispatched
input event) and removes the per-keystroke race entirely.

Two fixes:
1. safeFill switched from click + focus + Ctrl+A + Delete + keyboard.type
   to locator.fill, with toHaveValue staying as the commit gate.
2. Pre-submit re-assertion in the inline reply scenario — fail loudly
   on a state drop instead of silently sending an empty-body POST that
   the response filter rejects.

Local: TC-MSG-009 passes in 6.3s.

* fix(auth,ui): address sidebar customization review findings

Resolves the P1/P2 findings from the haxiorz auto-review on PR open-mercato#1730.

- Switch all read paths in sidebarPreferencesService and the preferences /
  variants API routes from raw em.find/em.findOne to findWithDecryption /
  findOneWithDecryption with explicit { tenantId, organizationId } scope so
  tenant data encryption helpers run consistently and the project's
  encryption-aware ORM rule holds (P1 #2).
- Wrap every write in SidebarCustomizationEditor (variant POST/PUT,
  delete, toggleActive, preferences PUT) in useGuardedMutation.runMutation
  with a stable contextId so global mutation injections (record locks,
  conflict UI) run, and surface retryLastMutation in the injection
  context. The PUT preferences sync is now error-checked: a sync failure
  flashes the save error instead of silently flashing success while the
  AppShell sidebar reads the unsynced preference (P1 #3).
- Drop the locale predicate from both nativeDelete sites in the sidebar
  preferences route (PUT clearRoleIds path + DELETE handler). Save and
  load helpers are cross-locale (unique key (role, tenantId)); filtering
  delete by locale orphaned rows created under another locale (P1 #4).
- Add the three missing AppShell search keys (searchNavPlaceholder,
  searchNavAria, searchNavClear) to en/pl/de/es, and remove seven dead
  appShell.sidebar* keys that were never referenced from any source
  file (P2 #5).

* test(qa): extend TC-CRM-007 + TC-INT-002 timeouts for CI shard 6 load

Both deal-creation specs were timing out on CI shard 6/15 with three
deterministic failures across reruns:
  - TC-CRM-007: timedOut at selectByFieldId clicking a still-disabled
    Status combobox (DictionaryEntrySelect.loading > 20s under shard load)
  - TC-INT-002: failed at toHaveURL('/customers/deals$') because Title
    was empty + "This field is required" — a late dictionary load
    re-triggered CrudForm's initialValues merge and clobbered the typed
    value before submit, so validation rejected the request

Both tests pass in ~3-8s locally in the ephemeral Docker environment
with the same code, so the regression is purely CI shard 6 parallel
load competing with 49 other specs for resources. Match the proven
TC-MSG-009 fix pattern (commit ac37d01):
  - test.setTimeout(120_000 / 180_000) per test
  - expect(combobox).toBeEnabled({ timeout: 30_000 }) before every
    selectByFieldId click — gates on dictionary load completion
  - expect(titleInput).toHaveValue(...) immediately after fill —
    atomic confirmation the controlled state has committed
  - defensive title re-fill right before submit so a late initialValues
    merge that clobbers the value still produces a valid POST
  - expect(option).toBeVisible() before option click — gates on
    Radix portal mount

Test-only change; no application code touched. Verified locally with
yarn test:integration:ephemeral on both specs — 2 passed (22.9s).

* fix(auth,ui): address Patryk review findings on sidebar customization

Resolves the High and Medium findings from the @patrykk-com review on PR open-mercato#1730.

High:
- Migration-snapshot drift on sidebar_variants: the snapshot still listed the
  legacy `sidebar_variants_user_id_tenant_id_locale_name_unique` constraint
  even though Migration20260427124900 + 20260427143311 dropped it and replaced
  it with a partial unique index `WHERE deleted_at IS NULL` (which a `@Unique`
  decorator cannot represent). Drop the @unique decorator on `SidebarVariant`
  and remove the stale snapshot entry; partial index is owned by raw SQL in
  the migration. A follow-up `yarn db:generate` now diffs cleanly. (H #1)
- Move inline zod schemas (sidebarSettingsSchema, createVariantInputSchema,
  updateVariantInputSchema, variantRecordSchema) from variants route handlers
  into `data/validators.ts` and import them in both routes. Settings shape is
  shared with `sidebarPreferencesInputSchema` so the constraint definitions
  no longer drift. (H #2)

Medium:
- Replace `as any` / `: any` across the new sidebar code with `EntityManager`
  + typed `FilterQuery`. `parsed.data.settings as any` casts are gone now
  that service signatures accept `Partial<SidebarPreferencesSettings>` (which
  matches the inferred zod type). (M #3)
- Add explicit one-line rationale on every empty-catch block in AppShell
  (localStorage / cookie blocked in private mode — non-critical) and
  SidebarCustomizationEditor (`window.dispatchEvent` with no listener —
  AppShell refreshes on next navigation). (M #4)
- Replace raw `<button>` drag handle in SortableItemRow with `<IconButton
  variant="ghost" size="sm">` and use the existing forwardRef so
  `setActivatorNodeRef` and dnd-kit listeners still wire correctly. (M #5)
- i18n hardcoded strings in SidebarPreview (`Search...`, `No groups to
  preview.`, `Drag to reorder`) — wrapped in `t(...)` and added 3 new keys
  to en/pl/de/es. (M #6)
- Switch primitive: replace inline `shadow-[0_1px_2px_rgba(10,13,20,...)`
  arbitrary-value shadow with the new `--shadow-switch-thumb` CSS custom
  property in light + dark themes (and synced into the standalone template
  globals.css). Switch now uses `shadow-switch-thumb` Tailwind utility. (M #7)
- Behavior regression for non-admin users: `requireFeatures:
  ['auth.sidebar.manage']` on the sidebar-customization page meta locked
  every non-admin user out of personal-scope customization, even though the
  variants/preferences APIs only gate role-application via that feature.
  Drop the page-level requireFeatures so any authenticated user can reach
  the page; the editor already conditionally hides "Apply to roles" via
  `canApplyToRoles` (server-checked against `auth.sidebar.manage`). (M #8)

New tests:
- 6 unit tests in `sidebarPreferencesService.scope.test.ts` lock down the
  cross-tenant + cross-user scope guards on `loadSidebarVariant`,
  `updateSidebarVariant`, `deleteSidebarVariant`. Each test stubs
  `findOneWithDecryption` and asserts the exact `{ id, user, tenantId,
  deletedAt: null }` filter shape so a future refactor can't silently
  drop the user or tenant filter. (M #9)

All 405 core test suites (3,329 tests) and 71 UI test suites (363 tests)
pass; build:packages clean across 18 packages.

* fix(auth): align sidebar preferences snapshot with partial unique indexes

UserSidebarPreference and RoleSidebarPreference still carried @unique
decorators that included locale, so the MikroORM snapshot kept the old
locale-scoped unique constraints even though Migration20260427143311
replaced them with partial unique indexes scoped to live rows. The next
yarn db:generate would have emitted a fixup migration trying to drop a
constraint already gone and add one colliding with the partial index.

Mirror the SidebarVariant approach: drop the @unique decorators
(partial indexes can't be expressed via the decorator), document the
ownership in raw SQL, and remove the stale unique entries from the
snapshot so it reflects the post-143311 state. yarn db:generate now
reports auth: no changes.

* test(auth,ui): add SidebarCustomizationEditor unit smoke test

The spec at .ai/specs/2026-04-27-ds-sidebar-customization-page.md
required SidebarCustomizationEditor.test.tsx covering load/save/cancel
flows, error states, role-apply target rendering, and drag-handle DOM
presence. Service-layer scope guards and Playwright integration tests
already shipped, but the editor's React state transitions had no unit
coverage. Adds a 5-test smoke suite that mocks apiCall/flash/injection
and asserts: skeleton-before-data, drag handles after load, load-error
surfacing on 500, role-apply targets when canApplyToRoles=true, and
that the role list is hidden when canApplyToRoles=false.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: zielivia <zielivia@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
yokoszn pushed a commit that referenced this pull request Jun 16, 2026
Addresses the High + Medium + Low items from
@pkarw's auto-review-pr on open-mercato#1933.

High — Sheet side="left" CSS bug:
- packages/ui/src/primitives/sheet.tsx: replace the
  invalid `inset-y-[var(--topbar-height,0px)_0]` (which
  compiled to `top: var(--topbar-height,0px) 0; bottom: ...`
  — both rules dropped by the browser because top/bottom
  accept only ONE length value) with the same explicit
  `top-[var(--topbar-height,0px)] bottom-0` pair the
  `right` variant uses. The right variant is harmonized
  with the same shape for consistency.
- packages/ui/src/primitives/__tests__/sheet.test.tsx:
  add a regression test that asserts the resolved class
  string carries the two separate inset utilities and
  does NOT contain the invalid combined form.

Medium — DS arbitrary-value cleanup:
- AppShell.tsx: `z-[var(--z-index-sticky)]` →
  `z-sticky`; `z-[var(--z-index-dropdown)]` → `z-dropdown`
  (DS utility exists and is used in the same file below).
- ProfileDropdown.tsx, NotificationPanel.tsx,
  NotificationItem.tsx, OrganizationSwitcher.tsx (apps
  + template), TopbarSearchInline.tsx: `text-[10px]` /
  `text-[11px]` → `text-overline` (DS token,
  --font-size-overline: 0.6875rem). NotificationItem
  `gap-[15px]` → `gap-4`.

Medium — Mobile drawer ARIA Tabs:
- AppShell.tsx: link each `role="tab"` to the section
  panel via `id` + `aria-controls`; wrap the sidebar
  body in `role="tabpanel"` with `aria-labelledby`
  pointing at the active tab id. Screen readers can now
  reach the panel through the relationship.

Medium — Sheet --topbar-height JSDoc:
- sheet.tsx: add JSDoc on SheetContent explaining the
  --topbar-height contract and how consumers opt into
  the offset from a layout. The `0px` fallback is
  retained (works correctly outside AppShell — portal
  modals, marketing surfaces — instead of forcing a
  60px gap that they would have to compensate for).

Medium — AppShell Button anti-pattern:
- AppShell.tsx (topbar injected actions): drop the
  `h-7 text-xs` override on a `size="sm"` Button —
  matches the same-row siblings (default-sized icon
  buttons in the topbar) per packages/ui/AGENTS.md
  Critical MUST rule #3 / #5.

Low — TopbarSearchInline raw <button>:
- TopbarSearchInline.tsx: collapsed search trigger
  uses `IconButton` (variant="ghost", size="lg" →
  size-9 to match the prior raw button). The container
  ref is preserved via a wrapping <span ref={...}>
  since IconButton is not a forwardRef component
  (changing that is out of scope for this PR).
yokoszn pushed a commit that referenced this pull request Jun 16, 2026
…ies for ~1 GB dev RAM win (open-mercato#2129)

* docs(runs): add execution plan for dev-mode-lazy-load-heavy-clients

Seeds the run folder for the next round of dev-mode RAM savings, targeted
at ≥1 GB peak-RSS reduction via lazy-loading heavy client libraries
(recharts, @xyflow/react), deferring ClientBootstrap registry barrels,
and enabling Next.js optimizePackageImports.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* perf(ui): lazy-load recharts chart primitives via next/dynamic

Splits BarChart / LineChart / PieChart into a thin public wrapper that
renders loading/error/empty without touching recharts, plus a sibling
*Impl module loaded via next/dynamic({ ssr: false }). The recharts
module is no longer in the Turbopack module graph for routes that
import @open-mercato/ui/backend/charts but never render data, and for
the dashboards page itself the recharts chunk is now lazy-compiled on
first paint instead of at boot.

No public API change — BarChart / LineChart / PieChart export the same
props.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* perf(workflows): lazy-load @xyflow/react WorkflowGraph and scope its CSS

Splits WorkflowGraph.tsx into:
  - WorkflowGraphImpl.tsx — owns every @xyflow/react runtime import + the
    @xyflow/react/dist/style.css side-effect import. Default-exports the
    ReactFlow component.
  - WorkflowGraph.tsx — thin client wrapper. Loads the Impl via
    next/dynamic({ ssr: false }) and forwards props. Re-exports
    WorkflowGraphReadOnly as a thin convenience wrapper.

Removes the global @xyflow/react/dist/style.css import from
apps/mercato/src/app/globals.css; the stylesheet now ships in the lazy
chunk next to the component that needs it. Every route other than the
workflows visual editor / instance viewer no longer pays the xyflow
parse + style cost.

Type-only imports of Node / Edge / Connection in the wrapper are fully
erased at runtime, so no @xyflow/react code enters the wrapper's
chunk.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* perf(app): defer ClientBootstrap registry barrels to dynamic imports

ClientBootstrapProvider used to statically import five generated
registry barrels — injection-widgets, injection-tables,
enabled-module-ids, dashboard-widgets, notification-handlers — pulling
every module's client widget tree into the AppProviders chunk on every
route, including auth/login.

Now bootstrap() loads the five barrels via Promise.all of dynamic
imports inside the useEffect, so each barrel becomes its own lazy
chunk. Routes that mount this provider but never reach a widget
consumer no longer pay for the registries on initial page parse.

Also:
- deduplicate the double @/.mercato/generated/translations-fields
  side-effect import (was imported on lines 6 and 16).
- keep messages.client + payments.client + translations-fields static
  because they register component classes during import and consumers
  read them during the same paint.
- cache the bootstrap promise so the synchronous and useEffect entry
  points share one in-flight import.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* perf(next-config): enable optimizePackageImports for lucide-react, recharts, date-fns

Adds experimental.optimizePackageImports to apps/mercato/next.config.ts
naming three barrel-heavy packages:

  - lucide-react — 398 import sites across the repo, full icon pack
    (~1000 exports). The flag lets Turbopack evaluate only the icons
    each file imports.
  - recharts — 12 import sites; pairs with the next/dynamic split in
    packages/ui/src/backend/charts/*Impl.tsx so unused chart parts no
    longer parse.
  - date-fns — already uses deep imports everywhere in the repo;
    listing it is defense-in-depth.

The option is enabled in both dev and prod since the behaviour is the
same: rewrite barrel imports to deep imports at compile time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(next-config): remove dead transpiledWorkspacePackages computation

transpilePackages was permanently commented out in the config (commit
history shows the line landed disabled). The supporting computation —
parsing apps/mercato/package.json on boot, scanning
external/official-modules/packages/ for subdirectories — was still
running on every Next.js boot and feeding into a never-used array.

Drops:
- the `import fs` (now unused)
- the synchronous `JSON.parse(fs.readFileSync(...))` of package.json
- the `fs.readdirSync` walk of external/official-modules/packages/
- the unused `transpiledWorkspacePackages` array

Git history is canonical if we ever want to revive transpilePackages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(ui,workflows): cover lazy-loading boundaries for recharts and xyflow

Extends packages/ui/src/backend/__tests__/lazy-heavy-libraries.test.ts
with source-string assertions mirroring the existing ScheduleView /
react-markdown / remark-gfm guardrails:

  - BarChart / LineChart / PieChart wrappers MUST NOT statically import
    recharts; they MUST use next/dynamic to load their sibling *Impl.
  - The *Impl modules MUST contain the recharts runtime import.
  - WorkflowGraph wrapper MUST have only type-only imports from
    @xyflow/react and MUST dynamic-import WorkflowGraphImpl.
  - WorkflowGraphImpl MUST own the runtime @xyflow/react import + the
    @xyflow/react/dist/style.css side-effect import.
  - apps/mercato/src/app/globals.css MUST NOT contain the @xyflow CSS.

These tests freeze the lazy boundary so future edits cannot silently
regress dev-mode RAM by re-introducing the eager imports.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(specs): add dev-mode lazy-load heavy clients spec

Captures the interventions landed in this PR (lazy recharts, lazy
@xyflow/react, deferred ClientBootstrap registry barrels,
experimental.optimizePackageImports, dead transpiledWorkspacePackages
cleanup), the manual measurement methodology to use until the
yarn dev:profile harness from PR open-mercato#2104 lands, and a migration
playbook for future heavy browser-only libraries.

Also flips Phase D of the long-running frontend-client-boundary RAM
reduction spec from "Not Started" to "In Progress" and links to this
spec as the evidence.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(core): add ambient *.css module declaration

WorkflowGraphImpl.tsx now imports '@xyflow/react/dist/style.css' as a
side-effect (moved out of apps/mercato/src/app/globals.css in the
@xyflow/react lazy-load step). packages/core lacked a CSS ambient
declaration — Next.js apps get one via next-env.d.ts, but workspace
packages need their own.

Adds packages/core/src/global.d.ts with `declare module '*.css'` and
`declare module '*.scss'` so any future lazy-loaded client component
in packages/core can colocate its CSS side-effect import with the
runtime import.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(runs): mark dev-mode-lazy-load-heavy-clients run complete

Final gate recorded: yarn build:packages, generate, typecheck,
i18n:check-sync, i18n:check-usage, build:app all green. Unit tests:
1081/1081 in @open-mercato/ui, 455/455 in @open-mercato/core workflows,
13/13 in the new lazy-heavy-libraries guardrails. Full-monorepo
yarn test skipped due to janitor-worktree RAM cap (UI + workflows
covers every file touched).

PR open-mercato#2129 opened, claimed with three-signal lock, body + labels set.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(workflows): respect height prop in lazy WorkflowGraph loading placeholder

The next/dynamic loading callback cannot access wrapper props, so the
placeholder hardcoded height: '600px' regardless of the caller-supplied
height. Callers passing `height="100%"` or `height="calc(90svh - 80px)"`
saw a 600px flash before the impl chunk loaded.

Move the placeholder rendering into the wrapper component and gate it on
chunk readiness so the spinner box matches the eventual impl height.

Addresses CR finding #3 on PR open-mercato#2129.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: loading widgets race condition fixed

* fix: dashboard screen fix

* feat: template drift fix

* fix(ui): reload injected data widgets after async bootstrap registration

useInjectionDataWidgets now subscribes to injection-registry changes like
useInjectionWidgets, so injected menus, DataTable extensions, and CrudForm
fields populate even when ClientBootstrap registers the registries after the
hook first runs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(dev): keep client registry bootstrap retryable on lazy chunk failure

A failed dynamic import in clientBootstrap() rejected the cached promise and
was silently swallowed, leaving every client registry empty with no retry —
dashboard widget cards would then wait on registration forever. Catch the
failure, clear the cached promise so the next render retries, and log it.
Mirrored in the create-app template for parity.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Piotr Karwatka <211899+pkarw@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
yokoszn pushed a commit that referenced this pull request Jun 16, 2026
…09, bulk validation, i18n (open-mercato#2303, open-mercato#2304, open-mercato#2305) (open-mercato#2309)

* feat(staff): add timesheets backend — SPEC-069 Phase 1 (Steps 1-5)

Implements the backend foundation for timesheets inside the staff module:

- ACL: 7 new feature flags (view, manage_own, manage_all, projects.view/manage, approve, lock)
- Data: 4 entities (TimeEntry, TimeEntrySegment, TimeProject, TimeProjectMember) with indexes
- Validators: 9 Zod schemas including bulk save (max 200 entries)
- Commands: 8 undoable commands (CRUD entries, CRUD projects, assign/unassign members)
- Events: 8 new events (CRUD + timer_started/stopped)
- API: 8 route files (entries CRUD, bulk save, timer start/stop, segments, projects CRUD, employee assignments)
- Search: TimeProject indexed (name, code, description, type, cost_center)
- Interceptor: self-scope enforcement on dashboard widget data for non-admin users
- Utility: shared staffMemberResolver (userId → staffMemberId)

Note: TimeEntry.date uses native date type (not text) per pre-implementation analysis finding.
UI pages, dashboard widgets, and i18n keys to follow in subsequent commits.

* feat(staff): add timesheets UI — SPEC-069 Phase 1 (Steps 6-7)

- My Timesheets monthly grid with 3-query N+1 mitigation (my-projects endpoint)
- Projects admin: list, create, detail with collapsible employee cards + Add Employee modal
- Feature gating: employee sees read-only views, admin/superadmin gets full management
- Page metadata for RBAC: detail requires projects.view, create requires projects.manage
- Bug fixes: bulk route auth.sub, segment route imports, employees route URL parsing
- Fix sticky column transparency in grid (bg-muted/50 → bg-muted)

* fix(staff): fix timer widget and route params — SPEC-069 Phase 1 (Step 8)

Fix timer detection (check startedAt/endedAt instead of non-existent
timer_running field), fix URL param extraction for segments routes,
fix create response field name, add dashboard widgets (Hours by Project
+ Time Reporting), analytics config, self-scope enforcement, i18n keys,
and project page metadata.

* test(staff): add timesheets integration tests — SPEC-069 Phase 1 (Step 9)

8 Playwright tests covering time entry CRUD, timer start/stop,
segments, projects, bulk save, dashboard widget data with self-scope
enforcement, and UI smoke tests for grid, projects list, and widgets.

* refactor(staff): update analytics date type and improve segment creation

- Changed the date field type in analytics configuration from 'date' to 'timestamp'.
- Refactored segment creation in time entries and timer start routes to use a segmentData object for better readability and maintainability.
- Updated error handling in the timesheets page to correctly reference the response object.
- Enhanced time entry and project commands to ensure proper type handling for source and status fields.
- Added optional fields for startedAt and endedAt in the time entry validation schema.
- Expanded i18n files with new keys for timesheet functionality in multiple languages.

* feat(staff): enhance time entry ownership validation and improve timesheet routes

- Added ownership validation for time entries in POST and PATCH routes to ensure users can only manage their own entries.
- Integrated `getStaffMemberByUserId` function to retrieve staff member details based on user ID.
- Updated error handling to return appropriate responses for unauthorized access and entry not found scenarios.
- Modified bulk route to include `staffMemberId` in the query for better data integrity.
- Refactored timesheets page state management to improve handling of entry data and dirty state tracking.

* fix(tapestry): update response field names and enhance timesheet integration tests

- Changed response field names in time project and employee assignment routes for consistency.
- Enhanced timesheet integration tests to include setup and teardown for project and employee assignments.
- Added assignedStartDate to employee assignment fixture for better tracking of assignment dates.

* test(staff): skip dashboard widget visibility test due to missing DB entries

- Updated the TC-STAFF-022 integration test to skip the widget visibility check for Time Reporting and Hours by Project due to the absence of required dashboard_role_widgets DB entries in setup.ts.
- Noted that the test has been manually verified and will be re-enabled once the setup includes the necessary widget IDs in the default role configuration.

* fix(staff): address PR review — CrudForm, DataTable filters, UX polish

- Use CrudForm for project create/edit with shared projectFormConfig
- Add separate edit page with version history and delete support
- Replace inline editing with link to edit page
- Status filter via DataTable filters (not custom buttons)
- Empty state with "+ Add first project" action button
- Profile link points to self-service /staff/profile/create
- Fix response field names (timeProjectId, timeProjectMemberId)

* feat(staff): enhance MyTimesheetsPage with project management features

- Added state management for project management access.
- Implemented API call to check if the user can manage projects.
- Updated empty state messaging to provide context for users without assigned projects, differentiating between admin and employee roles.
- Enhanced UI with buttons for creating and viewing projects based on user permissions.

* feat(i18n): update translations for timesheet project management

- Added new keys for project management features in German, English, Spanish, and Polish.
- Enhanced messaging for users without assigned projects, including admin-specific instructions.
- Updated UI labels and error messages to improve clarity and user experience.

* feat(staff): add timesheets UX enhancements spec

* feat(staff): add weekly view, calendar picker, list view and UX improvements

- Weekly grid as default view with Mon-Sun columns
- Toggle between Weekly/Monthly view modes
- Calendar week picker dropdown with "This week"/"Last week" shortcuts
- List view showing entries grouped by day
- View switcher component (Timesheet/List view)
- Compact grid cells with numeric-only input validation
- Fix cross-month week/monthly toggle bug
- Auto-assign project creator on creation
- Fix 403 redirect for employee on project detail
- No full page reload on navigation (opacity fade instead)

* feat(staff): timesheets UX fixes for hackathon

- Timer: add missing staffMemberId to create payload (fixes "Failed to start timer")
- AddRowDropdown: rewrite with createPortal so dropdown overlays without layout shift or inner scroll
- Move Add row into table tbody (above Daily Total row)
- CreateProjectDialog: add embedded prop to CrudForm to hide duplicate FormHeader (single Create button now)
- i18n: remove leading "+" from addRow.trigger and addRow.createProject (Plus icon already renders the symbol) across en/de/es/pl
- Reorganize timesheet UI components from backend/staff/timesheets/components to lib/timesheets-ui

* feat(staff): persist grid membership with show_in_grid + remove row in My Timesheets

Adds show_in_grid column on staff_time_project_members (+ backfill), new
self-service PATCH endpoint, and X remove button with confirm dialog.
Closes a gap in the original UX spec where "+ Add row" was only local state.

* feat(staff): add project colors, sidebar timer indicator, and inline list descriptions

- Phase 3: color field (varchar 20) on staff_time_projects with 12-color palette
- ColorPicker component, ProjectColorDot rendered in grid/AddRow/Timer/ListView
- Auto-fallback color from project name hash (djb2)
- Sidebar timer indicator widget with pulsing dot, persists via sessionStorage
- Inline editable descriptions in ListView (click to edit, Enter/blur to save)
- show_in_grid column on staff_time_project_members with backfill migration
- Self-service PATCH /api/staff/timesheets/my-projects/{projectId} endpoint
- X button to remove rows from grid with confirm dialog
- Unit tests for colors.ts (13 passing)
- i18n keys for 4 languages (en/pl/es/de)

* fix(staff): show add row button when user has assignments but empty grid

Switch the empty state condition from projects.length to allAssignedProjects.length
so users who haven't opted any project into their grid still see the "+ Add row"
control instead of being stuck on the "create a project" screen.

* fix(staff): resolve DELETE employee from time project returning 400

The mapInput for the employees DELETE action read `raw` directly, but the
CRUD factory passes `raw = { body, query }` — so the id coming in the query
string never reached the zod parser. Align with the customers/people pattern:
read id from parsed.body.id ?? parsed.id ?? parsed.query.id ?? URL search params.

* feat(staff): add timesheets projects portfolio view (Phase A + B)

Redesign /backend/staff/timesheets/projects into a role-aware portfolio
with table and cards view modes. PM sees team-wide aggregates; Collaborator
sees personal hours scoped via mine=1 filter.

Backend
- New aggregate helpers: computeProjectsKpis, computeProjectHoursTrend,
  listProjectMembersPreview + 26 unit tests
- New GET /api/staff/timesheets/projects/kpis endpoint with role-aware
  PM/Collab response shapes (openApi + zod schemas)
- New response enricher staff.timesheets-projects-portfolio targeting
  staff:staff_time_project — adds _staff.{hoursWeek, hoursTrend,
  members, memberCount, myRole} via batched SQL (no N+1)
- Extend /api/staff/timesheets/time-projects with mine=1 filter and
  include query param

UI
- ProjectsKpiStrip (5 PM cards / 3 Collab cards with delta badges)
- SavedViewTabs (status + Mine, URL-synced)
- ViewModeToggle + useProjectsViewMode (localStorage persisted)
- ProjectCard, ProjectsCards grid (3 col)
- HoursSparkline (SVG, 7 weeks, theme-aware)
- ProjectMembersAvatarStack (max 4 + N overflow, dark mode palette)
- Enriched table columns: color dot, status badge, team/role, sparkline,
  relative updated-at
- Inline refresh (no skeleton flash on filter/search)
- Dark mode tokens across all new components + fix project name in
  My Timesheets grid

i18n: 31+ new staff.timesheets.projects.portfolio.* keys in
en/de/pl/es (DE/PL/ES placeholders pending translation)

Spec: .ai/specs/2026-04-24-timesheets-projects-portfolio-view.md
Pre-impl analysis: .ai/specs/analysis/

Lessons: QueryEngine doesn't support $or top-level filters; CRUD
factory's enricher feature gating expects rbac.getGrantedFeatures()
which RbacService doesn't expose — so enrichers with features array
are silently skipped. Routed ACL via route metadata + inline manage
check instead.

* fix(staff): prevent cross-employee time-entry leak on GET endpoint

Add self-scope interceptor for GET /api/staff/timesheets/time-entries that forces staffMemberId to the caller's own staff member when the user lacks staff.timesheets.manage_all (or staff.* wildcard).

Mirrors the existing self-scope pattern used by the dashboard widget endpoint. Extracts the wildcard ACL check into a shared helper to avoid duplication between both interceptors.

Fixes review finding #1 on PR open-mercato#1111.

* fix(staff): enforce unique constraint on project code and member assignment

MikroORM v6 silently drops the unique flag when the @Index options bag also carries a where clause. The original migration emitted plain 'create index' statements instead of 'create unique index', allowing duplicate project codes within an (org, tenant) and duplicate active assignments of the same staff member to one project.

Add a fix-up migration that drops the affected indexes and recreates them as partial unique indexes (where deleted_at is null) so reuse of codes after soft-delete keeps working.

Affected indexes: staff_time_projects_code_unique_idx, staff_time_project_members_unique_idx.

Fixes review finding #2 on PR open-mercato#1111.

* fix(dashboards): remove cross-module staff coupling from widgets/data route

The route was importing StaffTeamMember from the staff module and inlining self-scope enforcement for staff:staff_time_entries entityType, violating the architectural rule against cross-module ORM coupling.

Replace the inline check with a proper invocation of runApiInterceptorsBefore. The interceptor 'staff.timesheets.self-scope-widget-data' already declared in staff/api/interceptors.ts now runs effectively (until now it was registered but never invoked because custom routes do not auto-run interceptors).

Side effects: the route is now open to interceptor injection from any module, not just staff. The staff interceptor itself was not changed.

Fixes review finding #3 on PR open-mercato#1111.

* fix(staff): make bulk time-entries save atomic

Wrap the create/update/soft-delete loop in em.transactional so the whole batch is committed or rolled back as a unit. A mid-loop failure no longer leaves the database in a partial state.

Also move the existingEntries lookup inside the transaction to avoid a read-modify-write race between fetching current rows and applying mutations.

Fixes review finding #4 on PR open-mercato#1111.

* fix(staff): emit CRUD side effects from bulk time-entries save

The bulk endpoint mutated entities directly inside a single em.flush(), so the highest-traffic write path was silently skipping the staff.timesheets.time_entry.created/updated/deleted events, query index updates, and cache invalidation that the per-row commands provide.

Collect a per-row action log inside the transaction (created/updated/deleted), then after the transaction commits dispatch emitCrudSideEffects for each entity and flushCrudSideEffects once at the end. Events fire only after the DB changes are durable, per the side-effects guideline in core/AGENTS.md.

Fixes review finding #5 on PR open-mercato#1111.

* fix(staff): wire mutation guards into custom write routes

AGENTS.md requires every non-makeCrudRoute write to call validateCrudMutationGuard before mutating and runCrudMutationGuardAfterSuccess after success so record locks, conflict detection, and ACL-driven mutation policies actually fire. The six timesheets custom write routes shipped without it.

Add a thin staff/api/guards.ts helper around runMutationGuards + bridgeLegacyGuard (mirrors integrations/api/guards.ts) and wire it into:

- time-entries/bulk (POST) - update on staff.timesheets.time_entry

- time-entries/[id]/timer-start (POST) - update on staff.timesheets.time_entry

- time-entries/[id]/timer-stop (POST) - update on staff.timesheets.time_entry

- time-entries/[id]/segments (POST) - create on staff.timesheets.time_entry_segment

- time-entries/[id]/segments/[segmentId] (PATCH) - update on staff.timesheets.time_entry_segment

- my-projects/[projectId] (PATCH) - update on staff.timesheets.time_project_member

Each route now blocks on guard rejection (422 with the guard body) and dispatches afterSuccess callbacks after the flush succeeds.

Fixes review finding #6 on PR open-mercato#1111.

* fix(staff): drop vitest import from colors test

The colors test imported describe/it/expect from vitest, which is not a dependency, so the test could not run. Drop the import and rely on jest globals like the rest of the module.

Fixes review finding #7 on PR open-mercato#1111.

* fix(staff): extract pure helpers so unit tests can run

computeProjectsKpis and listProjectMembersPreview both imported MikroORM entities at module top-level, so the unit tests added for SPEC-069 Step 9 transitively loaded @mikro-orm/core ESM and exploded under the jest preset. The tests never executed.

Move the pure helpers into their own files that don't import entities:

- timesheets-projects/kpiMath.ts: deltaPct, minutesToHours

- timesheets-projects/initials.ts: computeInitials

Update the helper test files to import from the new pure modules. computeProjectsKpis and listProjectMembersPreview now import (and re-export computeInitials) from the new files so callers keep working.

Result: 26/26 helper tests now pass.

Fixes review finding #8 on PR open-mercato#1111.

* fix(staff): wire timesheets page writes through useGuardedMutation

The My Timesheets page is a custom backend page (not a CrudForm), so its four write call sites (bulk time-entries POST and three my-projects PATCH calls) bypassed the global mutation injection hooks. Record-lock conflict handling, scoped request headers, and the standard onBeforeSave/onAfterSave hooks never fired.

Wrap each write in runMutation({ operation, context, mutationPayload }) so global injection modules can run their before/after hooks and consume mutation errors consistently. Each call site passes a stable resourceKind plus the project or staff member id as resourceId.

Fixes review finding #9 on PR open-mercato#1111.

* fix(staff): use readJsonSafe consistently in timesheets routes

Three timesheets write routes still read JSON via req.json().catch(...) while the rest of the module already adopted readJsonSafe (see my-projects/[projectId]). Pick the conventional helper everywhere.

Affected:

- time-entries/bulk (POST)

- time-entries/[id]/segments (POST)

- time-entries/[id]/segments/[segmentId] (PATCH)

Fixes review finding #10 on PR open-mercato#1111.

* fix(staff): use resolveOrganizationScopeForRequest in segment PATCH

Every peer timesheets route resolves the active scope via resolveOrganizationScopeForRequest so org switching in multi-org tenants works. The segment PATCH was reading auth.tenantId/auth.orgId directly, so requests sent from a non-default organization landed on the wrong scope.

Reorder the handler to create the container before validating scope, then derive tenantId/organizationId from the resolver with the auth values as fallback, mirroring the bulk and timer routes.

Fixes review finding open-mercato#11 on PR open-mercato#1111.

* fix(staff): tighten projectId UUID validation in my-projects PATCH

The old /^[0-9a-f-]{36}$/i regex accepts any 36-character mix of hex and dashes (e.g. 36 dashes), so junk ids slipped through to the DB query. Replace it with z.string().uuid() so only well-formed UUIDs are accepted.

Fixes review finding open-mercato#12 on PR open-mercato#1111.

* fix(staff): use apiCallOrThrow for timesheets writes

Mixed patterns inside the same files: some writes used apiCallOrThrow / readApiResultOrThrow while others called apiCall and checked res.ok manually. Pick the convention so server error bodies propagate uniformly through raiseCrudError instead of a hand-rolled 'throw new Error(await res.response.text())'.

Updated:

- backend/staff/timesheets/page.tsx — 4 writes inside runMutation (bulk save + 3 my-projects PATCH)

- lib/timesheets-ui/TimerBar.tsx — 3 writes (time-entries POST, timer-start, timer-stop) consolidated into try/catch so the flash message paths are unchanged

Read-only GETs that use a fallback on failure stay on apiCall.

Fixes review finding open-mercato#13 on PR open-mercato#1111.

* fix(staff): sort i18n keys to satisfy i18n:check-sync

yarn i18n:check-sync was failing on the four staff locale files (en/pl/es/de) with 'unsorted keys'. Run --fix to reorder; no key or value content changes.

Fixes review finding open-mercato#14 on PR open-mercato#1111.

* test(staff): regression guard for time-entries self-scope leak

Spec §Security calls out the self-scope rule but no integration test covered it. Add TC-STAFF-023 so the GET cross-employee leak fixed in a7704babd cannot regress unnoticed.

The test logs in as admin to create a time entry owned by the admin's own staff member, then logs in as employee (manage_own only, no manage_all) and issues GET /api/staff/timesheets/time-entries?staffMemberId=<admin's id>. It asserts the admin entry never appears and that every returned row belongs to the employee — proof the staff/api interceptor rewrote the filter to the caller's own staff member id.

Self-contained: creates project + assignment + entry in setup, cleans up in finally.

Fixes review finding open-mercato#15 on PR open-mercato#1111.

* fix(staff): seed timesheets dashboard widgets into role defaults

The Time Reporting and Hours by Project widgets ship with defaultEnabled:false so the global dashboard seed never associated them with any role. Existing tenants ended up with the widgets installed but invisible — and TC-STAFF-022 had to skip in CI because nothing would render.

Wire staff/setup.ts seedDefaults to call appendWidgetsToRoles for superadmin, admin, and employee with both timesheets widget ids. appendWidgetsToRoles is idempotent and only adds missing ids, so re-running setup on existing tenants is safe. The dashboards module sits before staff in modules.ts, so the DashboardRoleWidgets rows it creates already exist when staff's seed runs.

Update TC-STAFF-022's skip comment to explain the seed is now in place; remove the skip once CI runs against a freshly-seeded tenant.

Fixes review finding open-mercato#16 on PR open-mercato#1111.

* docs(spec): clarify TimeProject.customer_id is optional

The Data Models table and Projects API contract both marked customer_id as required, but the actual entity and validator have always treated it as nullable — internal projects have no customer. The spec was the inconsistent side.

Update line 259 (Data Models) and line 421 (Create/Update fields) to call out the column as optional, and log the doc fix in the changelog.

Fixes review finding open-mercato#18 on PR open-mercato#1111.

* docs(spec): rename SPEC-069 file to date+slug convention

.ai/specs/AGENTS.md mandates {date}-{title}.md filenames and forbids new SPEC- prefixes; the timesheets spec was the lone outlier in this PR's surface. git mv preserves history, the README link is updated, and a changelog entry is added inside the spec. Textual references to 'SPEC-069' stay as a human identifier.

Fixes review finding open-mercato#19 on PR open-mercato#1111.

* docs(spec): log SPEC-069 filename normalization in changelog

* chore(staff): regenerate snapshot and lucide registry after develop rebase

* fix(staff): handle duplicate project code with 409 and validate timeProjectId in bulk save (closes open-mercato#2304)

* fix(staff): i18n timesheets relative time and aria-labels, add seed-timesheets-widgets CLI command (closes open-mercato#2305)

* fix(staff): use wildcard-aware hasFeature for manage_all ACL check (closes open-mercato#2303)

* fix(staff): load ACL via rbacService when JWT lacks features, sort i18n keys alphabetically

* fix(staff): enforce time-entry ownership on writes and emit timer lifecycle events (H-1, H-2)

* fix(staff): timesheets follow-up — M-1..M-4 from review (assign side-effects, indexer plumbing, ref validation, bulk stale-id 422)

---------

Co-authored-by: migsilva89 <migdrum@gmail.com>
yokoszn pushed a commit that referenced this pull request Jun 16, 2026
…ad of staying unhealthy for the instance lifetime (open-mercato#2661) (open-mercato#3016)

HashicorpVaultKmsService set healthy=false on the first transient Vault
error (timeout / network blip / 5xx) with no path back to true: the
instance never re-probed, retried, or reset. Because tenant-data
encryption fails open (encryptEntityPayload returns plaintext when
!isEnabled(), and isEnabled() ANDs kms.isHealthy()), a single transient
hiccup permanently downgraded all new writes for that instance to
plaintext — with only one console.warn as the signal.

Add a half-open circuit breaker:
- Distinguish a terminal misconfiguration (missing VAULT_ADDR/VAULT_TOKEN,
  sticky — only a restart fixes it) from transient infra failures.
- On a transient failure record the failure time; once VAULT_RECOVERY_COOLDOWN_MS
  (default 30s) elapses, isHealthy() reports healthy again so the next
  read/write re-probes Vault. A successful probe fully closes the breaker;
  a failing one re-opens it for another cooldown.
- A successful read (and a CAS-conflict write, which proves Vault is
  reachable) now also closes the breaker.

This bounds the plaintext-downgrade window from the whole instance lifetime
to a single cooldown interval and lets long-lived workers/subscribers recover
without a restart.

Scope note: this fixes the never-recover root cause (the issue title).
Converting the broader fail-open encryption path to fail-closed and adding
durable alerting (recommendation #3) is a separate behavioral/contract change
left as follow-up.

Tests: new self-healing breaker coverage (recovery after cooldown — fails
without the fix; terminal misconfig never heals; 404 read keeps Vault healthy
for read-before-write). Full encryption suite (60) green; tsc clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.