Skip to content

feat: free Magic Mark rebuilt on the Strapi Design System - #2

Merged
Schero94 merged 22 commits into
mainfrom
claude/magicmark-overhaul
Nov 7, 2026
Merged

Schero94 merged 22 commits into
mainfrom
claude/magicmark-overhaul

Conversation

@Schero94

@Schero94 Schero94 commented Nov 7, 2026

Copy link
Copy Markdown
Owner

Summary

Magic Mark is now free and rebuilt on the Strapi Design System. This branch removes the license system, secures every admin route on the server and redesigns the admin UI with Design System components only.

  • Free: the license guard, license and upgrade pages, feature gates, the 10/50 bookmark limit, the heartbeat and every call to the license server are gone. Stored licenseKey and lastValidated values are deleted once on bootstrap. There is no telemetry and the plugin makes no outbound requests.
  • Security: every admin route requires admin::isAuthenticatedAdmin plus admin::hasPermissions, and owners are checked in the service. Bookmarks you cannot see answer 404, foreign ones you can see answer 403. Requests are validated with strict zod schemas, and paths and queries are normalised with qs.
  • UI: the Content Manager list view gets a Bookmarks popover and an Advanced filters button. There is a bookmarks page (table, or cards below 1080 px), a create and edit dialog with sharing, a ConfirmDialog for deletes and a pinned bookmarks homepage widget. Everything works in dark mode and down to 320 px, in five languages.
  • Data: shares now live in their own table and pins are per user. 3.x data and permissions are migrated on the first start, safely when several processes start at once.
  • Tooling: ESLint flat config and Prettier, CI on Node 22 and 24, release.yml with an NPM_TOKEN fallback and provenance, Dependabot with verified auto-merge, GitHub Pages site.

Re-attack fixes in this round

  • MM-RA-1: bookmarks.manage now reaches every bookmark that is not private: public ones, ones shared with anyone (also when the manager is not a recipient) and ones without an owner. Managers list them under Show > All shared bookmarks (scope=managed, 403 without manage). Private bookmarks of other users stay hidden. The permission is now called "Manage shared bookmarks of others".
  • MM-RA-2: 3.x queries are normalised during the migration, so page and unknown keys are dropped. duplicate never copies an unnormalised query.
  • MM-RA-3: _q can now be up to 1000 characters. A rejected query shows a translated Alert in the save dialog and a matching notification for "update with current view".
  • MM-RA-4: the popover requests pinned bookmarks on their own and links to "Show all N bookmarks" (page filtered by content type) when a content type has more than 100.

Proof

  • Gates: lint, format:check, test (75/75), test:ts:front, test:ts:back, build, verify, verify:runtime all pass. The new tests fail without the fix (4 failures on the old code).
  • Live API re-check on an isolated Strapi 5.61.0 host (SQLite, egress blocked, synthetic data): 13/13 pass. These include: a manager who is not a recipient can GET and DELETE the shared bookmark of a deleted user; the private one answers 404; a recipient's DELETE answers 403; _q of 201 characters saves and 1001 answers 400 with fieldErrors.query; a pin on the 106th bookmark comes back from the pinned request.
  • Playwright (own Chromium) at 1440 and 320 px: 14/14 pass. Covered: pin visible in the popover, "Show all 106 bookmarks" link, managed scope, explanatory Alert, no horizontal overflow, no console errors from the plugin.
  • Site: 1440, 768, 390 and 320 px in light and dark mode, with no overflow and no broken anchors or images.
  • npm pack: 26 files, 101.3 kB, only dist, entry files, README, CHANGELOG and LICENSE. npm audit --omit=dev: 0 vulnerabilities.

Release impact

There is no BREAKING CHANGE footer, because 3.x permissions and data migrate automatically. semantic-release will therefore publish a minor version. Plugin-internal admin routes were removed (/license/*, /roles, /users, /bookmarks/reorder), and so were manual ordering and the 3.x action ids. The peer range for @strapi/strapi and @strapi/utils is now ^5.57.0, which matches the README. The owner should decide whether a major version fits better.

Open points

  • Not run with several real processes on PostgreSQL (target: 2 replicas x 4 pm2 workers). Concurrency was tested in unit tests and with parallel requests on SQLite.
  • The dev and peer tree still reports 83 transitive advisories, all inside @strapi/strapi 5.61.0. Every fix npm offers is a downgrade. TypeScript stays on 5.9 because @strapi/sdk-plugin 6.1.1 and typescript-eslint 8 require it.
  • Browser acceptance did not run the full measure.js contract (contrast, gap and height symmetry) or a full keyboard Tab walk. The 1280, 1080 and 520 px widths and reduced motion were not checked.
  • Not built (non-goals): saving list columns with a bookmark (MM-F09), a per-content-type default bookmark (MM-F16), and a database index on creatorId (MM-F17), which a content type schema cannot declare.
  • npm trusted publishing for this repository still has to be configured on npmjs.com. Until then releases use the NPM_TOKEN fallback.
  • Do not merge before review: merging to main publishes to npm.

🤖 Generated with Claude Code

Schero94 and others added 22 commits November 7, 2026 15:28
Imports the uncommitted Codex work as the overhaul base: Content Manager
filter depth guard, filterable attribute allowlist, percent-safe query
round trip, malformed filter parsing, dropdown positioning and log
redaction, with regression tests (39/39 pass, both type checks clean).

The release workflow change that removed the NPM_TOKEN fallback is not
imported; the fallback stays until npm trusted publishing is confirmed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- @strapi/strapi, @strapi/utils and @strapi/typescript-utils 5.61.0, qs 6.17.1,
  zod 4.6.7 (request validation), semantic-release plugins and Prettier latest.
- Runtime dependencies are qs and zod only. Design System, icons, React,
  React Router, styled-components, react-intl and @strapi/utils are peers
  supplied by the host; @types/qs moved to devDependencies.
- Removed: @heroicons/react (replaced by @strapi/icons), @swc/core (unused),
  the direct vite pin (@strapi/sdk-plugin 6.1.1 brings vite ^6.4.2) and
  @strapi/sdk-plugin as a peer (it is a build tool).
- Pins kept on purpose: React 18, React Router 6, styled-components 6 and
  react-intl 6 are the Strapi 5.61 admin peers; TypeScript stays on 5.9
  because @strapi/sdk-plugin 6.1.1 ships 5.9.3 and typescript-eslint 8.72
  supports TypeScript < 6.1.
- ESLint 10 flat config with typescript-eslint, react-hooks and
  eslint-config-prettier; lint, lint:fix, format and format:check scripts.
- strapi-server.js and strapi-admin.js load dist (the old file required a
  TypeScript source that is not published); verify:runtime checks the
  built server bundle; engines, publishConfig and files updated.
- Strict TypeScript in admin and server.

Docs: npm view (2026-11-07), typescript-eslint.io flat config, ESLint 10
flat config docs, @strapi/sdk-plugin 6.1.1 package.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Free:
- Remove the license guard, license controller and routes, license-check
  policy and middleware, the 10/50 bookmark limit and every call to the
  license server. No network calls, no timers, no device fingerprinting.
- Bootstrap deletes the stored licenseKey and lastValidated keys once.
- Remove COPYRIGHT_NOTICE.txt (it forbade removing the license system;
  the plugin is MIT) and the ad-hoc test-magic-mark.js script.

Security and function:
- Every admin route uses admin::isAuthenticatedAdmin and
  admin::hasPermissions; auth.scope was ignored by the admin strategy, so
  any admin could call every route (MM-F01, SEC-01).
- New actions bookmarks.read/create/update/delete/share/manage. Roles that
  held the 3.x settings.read/settings.update actions get the matching new
  actions in the afterSync hook, before the admin removes unknown actions.
- zod validation for every body and query (strict, bounded); list view
  paths must be Content Manager list views and queries are normalised with
  qs (no page, no prototype keys, no backslash or fragment) (SEC-02/03/06).
- Sharing moves to bookmark-share rows keyed by documentId, so sharing with
  users works (MM-F02) and access is filtered in SQL with pagination,
  search, scope and content type filters; owner names load in one query
  (MM-F05, SEC-07). Recipients never see sharing lists.
- Pins are per user (bookmark-preference rows) and work on shared
  bookmarks (MM-F12). Duplicate, update-with-current-view endpoints.
- Not visible -> 404, visible but foreign -> 403; bookmarks.manage edits
  and deletes any visible bookmark (MM-F13).
- User search is paginated and returns email only with admin::users.read
  (MM-F14, SEC-04).
- Multi-instance safe: 3.x rows (relative paths, share JSON with numeric or
  documentId ids, global pins) are migrated with guarded updateMany claims
  and transactions; deleting admin users or roles cleans up shares, pins
  and private bookmarks through db lifecycles (unsubscribed in destroy).
- Errors use @strapi/utils classes; logs carry no keys or personal data.

Tests: server-service, server-validation, server-routes (real
hasPermissions policy), server-controller, server-lifecycle.

Docs: docs.strapi.io server-routes, server-policies-middlewares,
admin-permissions-for-plugins, document-service, error-handling;
installed @strapi/admin 5.61.0 strategies/admin.js, policies/hasPermissions.js,
bootstrap.js (cleanPermissionsInDatabase), @strapi/core Strapi.js
(afterSync hook order), @strapi/database transaction-context and
lifecycles; zod 4 flattenError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every element is a @strapi/design-system 2.2.4 component or the
@strapi/strapi/admin page API; no custom styled components, colour
literals or browser dialogs remain (about 9,000 lines of custom UI,
license screens and dead modules removed).

Content Manager list view (one injection, two icon buttons next to the
native View settings button):
- Advanced filters: DS Modal with one match choice (all/any) and a flat
  list of conditions (field Combobox, native operator wording, typed value
  inputs). "All" writes the native $and shape, so the conditions show as
  native filter tags; applying uses useQueryParams without a page reload
  (MM-F06, MM-F07, UX-03..UX-11). Fields come from /content-manager/init and
  the list configuration, so editors need no Content-Type Builder access,
  including relation main fields and component fields (MM-F04, MM-F15).
- Bookmarks: DS Popover with the bookmarks of this content type, pinned
  first, Save view, pin, update with current view and Manage bookmarks.
  Opening a bookmark replaces the Content Manager's remembered list
  settings for that type, so stale filters do not mix in (MM-F09); paths
  are router paths, so a custom admin URL works (MM-F08).

Bookmarks page: Page/Layouts header, SearchInput, scope and content type
selects, DS Table (cards below 768 px), admin Pagination, row pin and a
More menu (open, edit, duplicate, copy link, delete with ConfirmDialog),
EmptyStateLayout states, Page.Protect. Create/edit in a DS Modal with a
read-only summary of the saved view and Only me / roles and users / all
admins sharing with server-side user search.

Homepage widget "Pinned bookmarks" (feature-detected). No settings
section, no license pages, no keep-alive polling, no console output.
All strings in en, de, fr, es and pt; German without forms of address.

Tests: admin-filters (qs depth-5 round trips, native tag removal,
prototype pollution), admin-listview, admin-i18n (key parity, ICU,
placeholders, no Du/Sie, no emoji), source-contract.

Docs: installed @strapi/admin 5.61.0 Filters.mjs, useQueryParams.mjs,
WidgetHelpers, Widgets API, Pagination; @strapi/content-manager 5.61.0
ListViewPage.mjs, usePersistentQueryParams.mjs, InjectionZone.mjs,
routes/admin.js; @strapi/design-system 2.2.4 types (Modal, Popover,
Combobox, MultiSelect, Radio, Table, Tag); docs.strapi.io
admin-injection-zones, homepage widgets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- test.yml: Node 22 and 24, lint, format check, tests, blocking type checks,
  build, verify and verify:runtime; read-only token, concurrency, timeout,
  SHA-pinned actions, no persisted credentials (CI-1).
- release.yml: same checks plus a production audit before semantic-release;
  npm trusted publishing with the NPM_TOKEN fallback kept, provenance on,
  installs without lifecycle scripts, npx --no-install (REL-1). The
  .releaserc keeps failCommentCondition: false.
- pages.yml: SHA-pinned actions and job-scoped Pages permissions.
- Dependabot: weekly npm and GitHub Actions updates; Strapi peer majors
  and TypeScript majors are ignored (documented pins). Patch and minor
  updates are merged after the full verification (REL-2).
- Issue and pull request templates formatted with Prettier.
- tests/package.test.cjs checks dependencies, files, scripts, workflows.

Docs: GitHub Actions security hardening, Dependabot options reference,
semantic-release npm plugin (trusted publishing, provenance).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…all screens

- The admin router adds extra keys ("0") to ctx.params; the strict id
  schema rejected them, so GET /bookmarks/:id answered 400. Controllers now
  validate { id } only (found on the isolated 5.61 host; regression test).
- The bookmarks popover is limited to the available height and scrolls,
  so "Save view" stays reachable at 320 px; the pin button keeps its
  position whether or not "update with current view" is shown.
- Bookmark names in the table use the Design System Link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- README follows the workbench standard: screenshots (desktop, dark,
  390 px), core concepts, screens, compatibility with the documented peer
  and TypeScript pins, install, permission and route tables, security and
  privacy, troubleshooting, upgrade notes from 3.x, development and tests.
- GitHub Pages site documents the free plugin, permissions and routes with
  screenshots; the license and upgrade content is gone (FREE-7, DOC-1).
- New synthetic screenshots from the isolated Strapi 5.61 host replace the
  3.x images.
- CHANGELOG [Unreleased] with migration notes.
- Repository maintenance skill describes the free model and the gates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
npm audit fix (without --force) updates transitive dev dependencies:
88 -> 83 advisories, 0 critical, 0 in production dependencies
(npm audit --omit=dev: 0). The remaining highs sit inside @strapi/strapi
5.61.0 (its pinned zod, koa-session, koa-body and @strapi/* packages);
npm's only offered fix is a downgrade to Strapi 4, so they stay until
Strapi ships updates. Lint, tests, type checks, build and verify pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ters

/content-manager/init lists id and documentId for components. Components have no
documentId column, so a condition on it made the Content Manager list request
fail with a 500. Both internal keys are now skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… after delete

- Closing the create/edit dialog with unsaved changes (Escape, Close, Cancel) now
  opens a ConfirmDialog, like the native Content Manager Blocker; Cancel keeps the
  edits. A pristine form still closes directly. New message form.discardBody in
  en, de, fr, es and pt.
- After deleting a bookmark, focus moves to the page content (the Page.Main skip
  target) instead of falling back to <body> when the row disappears.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Design System Badge primary variant (primary700 on primary200) measures 2.4:1 in
the dark theme. Visibility badges now pass explicit token pairs that measure at
least 4.9:1 in both themes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At 768 px the six-column table scrolled horizontally and hid the row menu (the only
way to edit or delete). Between the medium and large breakpoints the Filters column
is now dropped, so the table fits; desktop keeps all columns, mobile keeps cards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… dialogs

- The pin toggle and the More actions menu now sit in separate table cells. A
  Design System table cell with more than one button calls preventDefault on every
  key event, including the Escape that bubbles through React from the portalled
  menu, so the menu stayed open. The menu is controlled like the native Content
  Manager row menu.
- Edit and delete dialogs opened from a row menu return focus to that row's More
  actions button when they close; after a delete, focus moves to the page content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Menu.Content defaults to a 15rem max height, so near the bottom of the viewport
the five actions scrolled and Open or Delete were hidden. The menu now has no max
height, like the native Content Manager row menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With separate pin and menu cells the table still overflowed by a few pixels at
768 px, hiding part of the row menu. Tablets now use the card layout that phones
already used; the full table starts at the Design System large breakpoint
(1080 px).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The trigger tooltip stayed open while the modal menu was open and covered the last
item (Delete) when the menu flipped above the button near the bottom of the
viewport. The trigger keeps its label as the accessible name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Synthetic data from the isolated 5.61 host, taken after the keyboard, contrast and
tablet fixes (opened and checked: example.test users only, no tokens or paths).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… saving

- Holders of bookmarks.manage now reach every bookmark that is not private:
  public ones, ones shared with anyone and ones without an owner, so shared
  bookmarks of deleted users can be removed. They are listed with the new
  'managed' scope (Show > All shared bookmarks); private bookmarks of other
  users stay hidden. The permission is renamed to 'Manage shared bookmarks
  of others' (MM-RA-1).
- 3.x queries are normalised during the migration (page and unknown keys
  dropped) and duplicate never copies an unnormalised query (MM-RA-2).
- A search text of up to 1000 characters can be saved; when the server
  still rejects a view, the dialog and the update action say why (MM-RA-3).
- The Bookmarks popover requests pinned bookmarks separately and links to
  the bookmarks page when a content type has more than 100 (MM-RA-4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ange

The README and site already said 5.57 or newer; the peer range said ^5.0.0.
Both @strapi/strapi and @strapi/utils peers are now ^5.57.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…E and site

README: hero screenshot, feature list, one-minute quick start, examples,
performance and multi-instance notes, FAQ and contributing guide.
Site: dark mode via prefers-color-scheme, Open Graph and canonical tags,
FAQ and multi-instance sections. Checked at 1440, 768, 390 and 320 px in
light and dark: no overflow, no broken anchors or images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Schero94
Schero94 merged commit e83b7c0 into main Nov 7, 2026
4 checks passed
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.

1 participant