Skip to content

feat(gdpr): pad deletion controls (PR1 of #6701)#7546

Merged
JohnMcLear merged 19 commits intodevelopfrom
feat-gdpr-pad-deletion
May 1, 2026
Merged

feat(gdpr): pad deletion controls (PR1 of #6701)#7546
JohnMcLear merged 19 commits intodevelopfrom
feat-gdpr-pad-deletion

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Summary

  • One-time sha256-hashed deletion token, surfaced plaintext once on pad creation
  • allowPadDeletionByAllUsers flag (defaults to false) to widen deletion rights
  • Three-way auth on socket PAD_DELETE and REST deletePad: creator cookie, valid token, or settings flag
  • Browser creators see a one-time token modal and can later delete via a recovery-token field under the existing Delete button

First of the five GDPR PRs outlined in #6701. Remaining scope (IP audit, identity hardening, cookie banner, author erasure) stays in follow-ups.

Design spec: docs/superpowers/specs/2026-04-18-gdpr-pr1-deletion-controls-design.md
Implementation plan: docs/superpowers/plans/2026-04-18-gdpr-pr1-deletion-controls.md

Test plan

  • pnpm --filter ep_etherpad-lite run ts-check
  • Backend: mocha tests/backend/specs/padDeletionManager.ts tests/backend/specs/api/deletePad.ts — 14 tests pass
  • Frontend: npx playwright test pad_deletion_token --project=chromium — 3 tests pass

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Apr 18, 2026

Review Summary by Qodo

(Agentic_describe updated until commit baa11b5)

feat(gdpr): pad deletion controls with one-time recovery tokens (PR1 of #6701)

✨ Enhancement

Grey Divider

Walkthroughs

Description
• One-time sha256-hashed deletion token returned plaintext on pad creation
• Three-way authorization for pad deletion: creator cookie, valid token, or settings flag
• Browser creators see one-time token modal and can delete via recovery-token field
• Comprehensive backend and frontend test coverage for token lifecycle and authorization matrix
Diagram
flowchart LR
  CreatePad["createPad/createGroupPad"]
  TokenMgr["PadDeletionManager"]
  DB["Database"]
  Response["API Response + clientVars"]
  Modal["Token Modal"]
  Delete["Delete Pad"]
  Auth["3-way Authorization"]
  
  CreatePad -->|createDeletionTokenIfAbsent| TokenMgr
  TokenMgr -->|store sha256 hash| DB
  TokenMgr -->|return plaintext once| Response
  Response -->|browser creator| Modal
  Modal -->|copy & acknowledge| Delete
  Delete -->|socket PAD_DELETE| Auth
  Auth -->|creator cookie OR valid token OR flag| Delete
Loading

Grey Divider

File Changes

1. src/node/db/PadDeletionManager.ts ✨ Enhancement +48/-0

New module for token creation, validation, and removal

src/node/db/PadDeletionManager.ts


2. src/node/db/API.ts ✨ Enhancement +15/-2

Return deletionToken from createPad and validate on deletePad

src/node/db/API.ts


3. src/node/db/GroupManager.ts ✨ Enhancement +11/-2

Return deletionToken from createGroupPad

src/node/db/GroupManager.ts


View more (19)
4. src/node/db/Pad.ts ✨ Enhancement +2/-0

Clean up deletion token when pad is removed

src/node/db/Pad.ts


5. src/node/handler/PadMessageHandler.ts ✨ Enhancement +41/-32

Three-way auth for socket PAD_DELETE and thread token to clientVars

src/node/handler/PadMessageHandler.ts


6. src/node/handler/APIHandler.ts ✨ Enhancement +1/-1

Advertise optional deletionToken parameter in API schema

src/node/handler/APIHandler.ts


7. src/node/utils/Settings.ts ⚙️ Configuration changes +2/-0

Add allowPadDeletionByAllUsers flag with false default

src/node/utils/Settings.ts


8. src/static/js/types/SocketIOMessage.ts ✨ Enhancement +3/-0

Add optional padDeletionToken to ClientVarPayload and PadDeleteMessage

src/static/js/types/SocketIOMessage.ts


9. src/static/js/pad.ts ✨ Enhancement +34/-0

Show one-time token modal and clear token after acknowledgement

src/static/js/pad.ts


10. src/static/js/pad_editor.ts ✨ Enhancement +27/-0

Wire delete-by-token input to socket PAD_DELETE message

src/static/js/pad_editor.ts


11. src/templates/pad.html ✨ Enhancement +23/-0

Add token modal and delete-with-token disclosure markup

src/templates/pad.html


12. src/static/skins/colibris/src/components/popup.css Formatting +37/-0

Style modal and delete-with-token disclosure layout

src/static/skins/colibris/src/components/popup.css


13. src/locales/en.json 📝 Documentation +8/-0

Add i18n strings for token modal and delete-with-token UI

src/locales/en.json


14. src/tests/backend/specs/padDeletionManager.ts 🧪 Tests +104/-0

Unit tests for token creation, validation, and removal

src/tests/backend/specs/padDeletionManager.ts


15. src/tests/backend/specs/api/deletePad.ts 🧪 Tests +77/-0

REST API tests for authorization matrix and token validation

src/tests/backend/specs/api/deletePad.ts


16. src/tests/frontend-new/specs/pad_deletion_token.spec.ts 🧪 Tests +84/-0

Playwright tests for modal display and delete-by-token flow

src/tests/frontend-new/specs/pad_deletion_token.spec.ts


17. src/tests/frontend-new/helper/padHelper.ts 🧪 Tests +15/-0

Auto-dismiss token modal in test helper to avoid blocking tests

src/tests/frontend-new/helper/padHelper.ts


18. settings.json.template ⚙️ Configuration changes +7/-0

Add allowPadDeletionByAllUsers setting with false default

settings.json.template


19. settings.json.docker ⚙️ Configuration changes +7/-0

Add allowPadDeletionByAllUsers setting with environment variable support

settings.json.docker


20. docs/superpowers/specs/2026-04-18-gdpr-pr1-deletion-controls-design.md 📝 Documentation +207/-0

Design specification for GDPR deletion controls feature

docs/superpowers/specs/2026-04-18-gdpr-pr1-deletion-controls-design.md


21. docs/superpowers/plans/2026-04-18-gdpr-pr1-deletion-controls.md 📝 Documentation +939/-0

Detailed implementation plan with 13 TDD-structured tasks

docs/superpowers/plans/2026-04-18-gdpr-pr1-deletion-controls.md


22. doc/api/http_api.md Additional files +21/-3

...

doc/api/http_api.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Apr 18, 2026

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (3) 📎 Requirement gaps (0)

Context used

Grey Divider


Action required

1. Deletion flag unusable in UI 🐞 Bug ≡ Correctness ⭐ New
Description
allowPadDeletionByAllUsers is only honored when no deletionToken is supplied, but the only
non-creator UI control always supplies a token, so enabling the flag still doesn’t let regular
editors delete via the UI. This is also inconsistent with the REST deletePad behavior, which
bypasses token checks when the flag is enabled even if a bogus token is provided.
Code

src/node/handler/PadMessageHandler.ts[R268-278]

+  const suppliedToken = padDeleteMessage.data.deletionToken;
+  const tokenSupplied = typeof suppliedToken === 'string' && suppliedToken !== '';
+  const tokenOk = tokenSupplied &&
+      await padDeletionManager.isValidDeletionToken(padId, suppliedToken);
+  // When a token is supplied it must validate. We deliberately do NOT fall
+  // back to the creator-cookie path, otherwise a creator pasting a wrong
+  // recovery token into the disclosure field would still succeed — masking a
+  // typo and contradicting the UI.
+  const creatorOk = !tokenSupplied && isCreator;
+  const flagOk = !tokenSupplied && !isCreator && settings.allowPadDeletionByAllUsers;
+
Evidence
Socket deletion allows the flag only when tokenSupplied is false; the UI token-delete path always
supplies a token; and non-creators don’t get the no-token delete button because canEditPadSettings
is creator-only. Meanwhile, REST deletePad ignores token validity when allowPadDeletionByAllUsers is
true, creating inconsistent behavior.

src/node/handler/PadMessageHandler.ts[258-302]
src/node/handler/PadMessageHandler.ts[1123-1127]
src/static/js/pad_editor.ts[140-168]
src/templates/pad.html[256-264]
src/node/db/API.ts[543-552]
settings.json.template[478-483]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`allowPadDeletionByAllUsers` is intended to allow any editor to delete without needing the one-time token, but the socket handler only applies the flag when *no token is supplied*. The UI’s non-creator delete path always supplies a token, so non-creators cannot delete via UI even when the flag is enabled.

### Issue Context
- Socket handler: `flagOk` is gated on `!tokenSupplied`.
- UI token-delete always sends `deletionToken`, so `tokenSupplied` is always true.
- Non-creators don’t have a UI path to send `PAD_DELETE` without a token because `canEditPadSettings` is creator-only.
- REST deletePad bypasses validation when the flag is enabled, even if a bogus token is supplied, so socket/REST are inconsistent.

### Fix Focus Areas
- src/node/handler/PadMessageHandler.ts[258-302]
- src/static/js/pad_editor.ts[140-200]
- src/node/handler/PadMessageHandler.ts[1123-1165]
- src/templates/pad.html[256-264]
- src/node/db/API.ts[543-552]

### Suggested approach
Choose one consistent policy and implement it across socket + UI:
1) **Server-side simplest:** If `settings.allowPadDeletionByAllUsers` is true, allow deletion regardless of `deletionToken` presence/validity (ignore token like REST does), and only use token validation when the flag is false.
2) **UI-side alternative:** Pass `allowPadDeletionByAllUsers` (or a `canDeletePad` boolean) via `clientVars` and, when enabled, have the UI send `PAD_DELETE` without `deletionToken` (or omit it) so the existing `flagOk` branch is reached.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Group pads leak deletion token 🐞 Bug ⛨ Security ⭐ New
Description
createGroupPad always creates/returns a deletion token, even when settings.requireAuthentication is
enabled, contradicting createPad’s explicit decision to suppress token issuance in that mode. This
reintroduces the “extra surface to leak” for group pads and creates inconsistent behavior between
createPad and createGroupPad.
Code

src/node/db/GroupManager.ts[R170-173]

+  return {
+    padID,
+    deletionToken: await padDeletionManager.createDeletionTokenIfAbsent(padID),
+  };
Evidence
createPad suppresses deletion token issuance when requireAuthentication is true, but createGroupPad
returns a plaintext deletionToken unconditionally; APIHandler routes createGroupPad calls through
API.ts which exports GroupManager.createGroupPad directly.

src/node/db/API.ts[508-529]
src/node/db/API.ts[46-52]
src/node/db/GroupManager.ts[140-173]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Group-pad creation (`createGroupPad`) always issues and returns a plaintext deletion token, even when `settings.requireAuthentication` is enabled. This contradicts `createPad`, which explicitly avoids issuing a token in that mode to reduce credential leakage surface.

### Issue Context
- `API.createPad` returns `deletionToken: null` when `settings.requireAuthentication` is true.
- `API.createGroupPad` is currently a direct export of `GroupManager.createGroupPad`, which always calls `createDeletionTokenIfAbsent()` and returns the plaintext.

### Fix Focus Areas
- src/node/db/GroupManager.ts[140-173]
- src/node/db/API.ts[46-52]
- src/node/db/API.ts[508-529]

### Suggested fix
Mirror `createPad` behavior:
- Import `settings` into `GroupManager.ts`.
- If `settings.requireAuthentication` is true, return `{padID, deletionToken: null}` and **do not** call `createDeletionTokenIfAbsent`.
- Keep the return type consistent (`string | null`) and update any related docs/tests if necessary.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. createPad response shape changed 📘 Rule violation ⚙ Maintainability
Description
createPad now returns data: {deletionToken: ...} (or {deletionToken: null}) instead of `data:
null`, changing the response schema for existing API consumers. This is a potentially breaking
public API change that should be avoided or versioned/opt-in and clearly documented as breaking.
Code

src/node/db/API.ts[523]

+  return {deletionToken: await padDeletionManager.createDeletionTokenIfAbsent(padID)};
Evidence
PR Compliance ID 11 requires avoiding unnecessary breaking changes to public APIs (or
documenting/versioning them). The code now returns an object from createPad (changing the data
type), and the HTTP API documentation is updated from data: null to an object containing
deletionToken, confirming the externally-visible response shape change.

src/node/db/API.ts[518-524]
doc/api/http_api.md[519-536]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `createPad` API response `data` changes from `null` to an object containing `deletionToken`, which can break clients that assume `data` is always `null` for this endpoint.
## Issue Context
This is a public HTTP API behavior change. To comply, preserve backward compatibility (or introduce a versioned/opt-in path).
## Fix Focus Areas
- src/node/db/API.ts[518-524]
- doc/api/http_api.md[519-536]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (3)
4. Deletion token not feature-flagged 📘 Rule violation ☼ Reliability
Description
Pad deletion tokens (API response + creator modal) are enabled unconditionally, changing default
behavior rather than being gated behind a disabled-by-default feature flag. This violates the
requirement that new features be behind an off-by-default flag to preserve pre-change behavior
unless explicitly enabled.
Code

src/node/handler/PadMessageHandler.ts[R1004-1012]

+    // Only the original creator of the pad (revision 0 author) receives the
+    // deletion token, and only on their first arrival — subsequent visits get
+    // null because createDeletionTokenIfAbsent() only emits a plaintext token
+    // once. Readonly sessions never see it.
+    const isCreator =
+        !sessionInfo.readonly && sessionInfo.author === await pad.getRevisionAuthor(0);
+    const padDeletionToken = isCreator
+        ? await padDeletionManager.createDeletionTokenIfAbsent(sessionInfo.padId)
+        : null;
Evidence
PR Compliance ID 5 requires new functionality to be behind a feature flag and disabled by default.
The PR unconditionally generates and returns a deletion token on pad creation and surfaces it to
creator sessions via clientVars without any enabling flag, so the feature is on by default.

src/node/db/API.ts[520-546]
src/node/handler/PadMessageHandler.ts[1004-1012]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Deletion tokens are generated and surfaced by default (API response and UI modal) without a feature flag. Compliance requires new features to be behind a disabled-by-default flag, with the default path matching pre-change behavior.
## Issue Context
Current behavior:
- `createPad()` always returns `{deletionToken: ...}`
- Creator sessions always attempt token creation and receive `clientVars.padDeletionToken`
## Fix Focus Areas
- src/node/db/API.ts[520-546]
- src/node/handler/PadMessageHandler.ts[1004-1012]
- src/node/utils/Settings.ts[172-176]
- settings.json.template[480-486]
- settings.json.docker[483-489]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. HTTP API docs not updated 📘 Rule violation ⚙ Maintainability
Description
The PR changes the public HTTP API (createPad now returns deletionToken; deletePad accepts
optional deletionToken) but the HTTP API documentation still describes the old signatures/returns.
This can cause integrator confusion and violates the requirement to update docs in the same PR when
changing APIs.
Code

src/node/db/API.ts[R520-546]

// create pad
await getPadSafe(padID, false, text, authorId);
+  return {deletionToken: await padDeletionManager.createDeletionTokenIfAbsent(padID)};
};
/**
-deletePad(padID) deletes a pad
+deletePad(padID, [deletionToken]) deletes a pad
Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
+{code: 1, message:"invalid deletionToken", data: null}
@param {String} padID the id of the pad
+ @param {String} [deletionToken] recovery token issued by createPad
*/
-exports.deletePad = async (padID: string) => {
+exports.deletePad = async (padID: string, deletionToken?: string) => {
const pad = await getPadSafe(padID, true);
+  // apikey-authenticated callers (no deletionToken supplied) are trusted.
+  // When a caller supplies a deletionToken, it must validate unless the
+  // instance has opted everyone in via allowPadDeletionByAllUsers.
+  if (deletionToken !== undefined && deletionToken !== '' &&
+      !settings.allowPadDeletionByAllUsers &&
+      !await padDeletionManager.isValidDeletionToken(padID, deletionToken)) {
+    throw new CustomError('invalid deletionToken', 'apierror');
+  }
Evidence
PR Compliance IDs 10 and 11 require API changes to be documented in the same PR. The code now
returns a deletionToken from createPad and adds an optional deletionToken parameter and error
to deletePad, but doc/api/http_api.md still documents createPad returning data: null and
deletePad(padID) with no token parameter.

src/node/db/API.ts[520-548]
src/node/handler/APIHandler.ts[53-56]
doc/api/http_api.md[519-592]
Best Practice: Repository guidelines
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The HTTP API docs are out of date relative to the changed endpoints:
- `createPad` now returns `data: {deletionToken: string|null}`
- `deletePad` now accepts optional `deletionToken` and may return `invalid deletionToken`
## Issue Context
The OpenAPI schema arg list was updated, but the human-readable HTTP API documentation (`doc/api/http_api.md`) still shows the previous signatures and example returns.
## Fix Focus Areas
- src/node/db/API.ts[520-548]
- src/node/handler/APIHandler.ts[53-56]
- doc/api/http_api.md[519-592]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Token creation race 🐞 Bug ≡ Correctness
Description
createDeletionTokenIfAbsent() uses a non-atomic read-then-write, so concurrent calls for the same
pad can each return different plaintext tokens and overwrite the stored hash, making at least one
returned token permanently invalid.
Code

src/node/db/PadDeletionManager.ts[R13-20]

+exports.createDeletionTokenIfAbsent = async (padId: string): Promise<string | null> => {
+  if (await DB.db.get(getDeletionTokenKey(padId)) != null) return null;
+  const deletionToken = randomString(32);
+  await DB.db.set(getDeletionTokenKey(padId), {
+    createdAt: Date.now(),
+    hash: hashDeletionToken(deletionToken).toString('hex'),
+  });
+  return deletionToken;
Evidence
The token is issued via DB.db.get() followed by DB.db.set() with no locking/CAS, so two
concurrent callers can both observe “absent” and then store different hashes. This function is
called during creator CLIENT_READY and also on HTTP API pad creation, making concurrent invocation
plausible (e.g., multiple creator tabs loading simultaneously).

src/node/db/PadDeletionManager.ts[13-20]
src/node/handler/PadMessageHandler.ts[1008-1012]
src/node/db/API.ts[508-524]
src/node/db/GroupManager.ts[140-173]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`createDeletionTokenIfAbsent()` does a check-then-set on the DB key. Under concurrency, two calls for the same `padId` can both return different plaintext tokens, but only the last stored hash will validate, causing at least one client to save an unusable recovery token.
### Issue Context
This function is called from multiple entry points (creator `CLIENT_READY`, `createPad`, `createGroupPad`). Even without multi-process, two creator tabs can race.
### Fix Focus Areas
- src/node/db/PadDeletionManager.ts[13-21]
- src/node/handler/PadMessageHandler.ts[1008-1012]
### Suggested fix
Implement a per-`padId` in-memory mutex/in-flight promise map around the token creation path:
- Re-check existence after acquiring the lock.
- Ensure only one call performs the `set` and returns the plaintext token; concurrent callers should reliably return `null`.
If Etherpad can run multi-process in your deployment, document this limitation or consider a DB-level atomic primitive if available.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

7. createPad docs contradict behavior 🐞 Bug ⚙ Maintainability
Description
The HTTP API docs claim createPad can return {deletionToken: null} when the pad already exists,
but the implementation throws padID does already exist and does not return success data. This will
mislead API consumers about idempotency and response shape.
Code

doc/api/http_api.md[R527-536]

+`data.deletionToken` is a one-shot recovery token tied to this pad. It is
+returned in plaintext on the first call for a given padID and is `null` on
+subsequent calls (the token itself is stored on the server as a sha256 hash).
+Pass it to **deletePad** (or the socket `PAD_DELETE` message) to delete the
+pad without the creator's author cookie.
+
*Example returns:*
-* `{code: 0, message:"ok", data: null}`
+* `{code: 0, message:"ok", data: {deletionToken: "…32-char random string…"}}`
+* `{code: 0, message:"ok", data: {deletionToken: null}}` — pad already existed
* `{code: 1, message:"padID does already exist", data: null}`
Evidence
The docs describe a successful (code 0) response for an already-existing pad, but the implementation
uses getPadSafe(..., shouldExist=false), which throws if the pad exists. Therefore that documented
response shape cannot occur.

doc/api/http_api.md[519-537]
src/node/db/API.ts[508-524]
src/node/db/API.ts[907-933]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`doc/api/http_api.md` documents a `{code: 0, ..., data: {deletionToken: null}}` outcome for `createPad` when the pad already exists, but the server code throws an error in that case.
### Issue Context
`createPad()` calls `getPadSafe(padID, false, ...)`, and `getPadSafe()` throws `padID does already exist` if the pad exists.
### Fix Focus Areas
- doc/api/http_api.md[519-537]
- src/node/db/API.ts[508-524]
- src/node/db/API.ts[907-933]
### Suggested change
- Either update the docs to remove/adjust the “pad already existed” success example, OR change `createPad` semantics to be idempotent (return code 0 with `{deletionToken: null}`) if that is the intended contract.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Delete token listeners persist 🐞 Bug ☼ Reliability
Description
The delete-by-token click handler adds persistent pad.socket.on('message') and
pad.socket.on('shout') listeners on every click and never removes them, so after a refused attempt
the page can accumulate handlers and later shouts can trigger multiple alert()s unrelated to
deletion.
Code

src/static/js/pad_editor.ts[R89-114]

+      // delete pad using a recovery token (second device / no creator cookie)
+      $('#delete-pad-token-submit').on('click', () => {
+        const token = String($('#delete-pad-token-input').val() || '').trim();
+        if (!token) return;
+        if (!window.confirm(html10n.get('pad.delete.confirm'))) return;
+
+        let handled = false;
+        pad.socket.on('message', (data: any) => {
+          if (data && data.disconnect === 'deleted') {
+            handled = true;
+            window.location.href = '/';
+          }
+        });
+        pad.socket.on('shout', (data: any) => {
+          handled = true;
+          const msg = data?.data?.payload?.message?.message;
+          if (msg) window.alert(msg);
+        });
+        pad.collabClient.sendMessage({
+          type: 'PAD_DELETE',
+          data: {padId: pad.getPadId(), deletionToken: token},
+        });
+        setTimeout(() => {
+          if (!handled) window.location.href = '/';
+        }, 5000);
+      });
Evidence
The new handler registers .on('message') and .on('shout') inside the click callback and does not
deregister them. If deletion is refused (shout path), the page stays loaded, so subsequent clicks
stack more listeners; existing listeners also alert() on any future shout payload because there is
no filtering or cleanup.

src/static/js/pad_editor.ts[89-114]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The delete-by-token flow attaches socket listeners on every click and never removes them, which can lead to accumulated handlers and repeated alerts/redirect behavior after multiple attempts.
### Issue Context
If the token is wrong, the server emits a shout and the page does not navigate away, so the user can retry and stack handlers.
### Fix Focus Areas
- src/static/js/pad_editor.ts[89-114]
### Suggested fix
- Use `.once()` instead of `.on()` for the temporary listeners.
- Or namespace and cleanup explicitly: `pad.socket.off('message.gdprDelete').on('message.gdprDelete', ...)` and similarly for `shout`, then remove them when handled or when the 5s timeout fires.
- Optionally filter the shout handler to only react to the specific refusal message/type used for pad deletion to avoid alerting on unrelated shouts.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

9. Empty token skips validation 🐞 Bug ⚙ Maintainability
Description
API.deletePad() treats deletionToken === '' as if no token was supplied, skipping validation and
proceeding to delete, which makes the parameter semantics ambiguous and unsafe if this path is ever
reused for non-admin deletion.
Code

src/node/db/API.ts[R542-546]

+  if (deletionToken !== undefined && deletionToken !== '' &&
+      !settings.allowPadDeletionByAllUsers &&
+      !await padDeletionManager.isValidDeletionToken(padID, deletionToken)) {
+    throw new CustomError('invalid deletionToken', 'apierror');
+  }
Evidence
The validation guard explicitly excludes the empty string, but the API handler passes query fields
positionally, so callers can send deletionToken= and bypass token validation logic in this
function. Today the REST API is authenticated, which limits impact, but the behavior contradicts the
notion of “if deletionToken is provided, validate it.”

src/node/db/API.ts[537-546]
src/node/handler/APIHandler.ts[35-62]
src/node/handler/APIHandler.ts[214-219]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`deletePad(padID, deletionToken?)` skips validation when `deletionToken` is an empty string, which makes the behavior inconsistent and potentially risky if reused for token-based deletion.
### Issue Context
`isValidDeletionToken()` already considers `''` invalid; the API wrapper currently avoids calling it for `''`.
### Fix Focus Areas
- src/node/db/API.ts[537-546]
### Suggested fix
Change the condition to validate any provided token, including empty strings:
- Use `if (deletionToken != null && !settings.allowPadDeletionByAllUsers && !await isValidDeletionToken(...)) throw ...;`
This preserves the “no token supplied” bypass for authenticated admin callers (undefined) while making `''` correctly fail.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit 703c5a3

Results up to commit baa11b5


🐞 Bugs (4) 📘 Rule violations (3) 📎 Requirement gaps (0)

Context used

Action required
1. createPad response shape changed 📘 Rule violation ⚙ Maintainability ⭐ New
Description
createPad now returns data: {deletionToken: ...} (or {deletionToken: null}) instead of `data:
null`, changing the response schema for existing API consumers. This is a potentially breaking
public API change that should be avoided or versioned/opt-in and clearly documented as breaking.
Code

src/node/db/API.ts[523]

+  return {deletionToken: await padDeletionManager.createDeletionTokenIfAbsent(padID)};
Evidence
PR Compliance ID 11 requires avoiding unnecessary breaking changes to public APIs (or
documenting/versioning them). The code now returns an object from createPad (changing the data
type), and the HTTP API documentation is updated from data: null to an object containing
deletionToken, confirming the externally-visible response shape change.

src/node/db/API.ts[518-524]
doc/api/http_api.md[519-536]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `createPad` API response `data` changes from `null` to an object containing `deletionToken`, which can break clients that assume `data` is always `null` for this endpoint.

## Issue Context
This is a public HTTP API behavior change. To comply, preserve backward compatibility (or introduce a versioned/opt-in path).

## Fix Focus Areas
- src/node/db/API.ts[518-524]
- doc/api/http_api.md[519-536]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Deletion token not feature-flagged 📘 Rule violation ☼ Reliability
Description
Pad deletion tokens (API response + creator modal) are enabled unconditionally, changing default
behavior rather than being gated behind a disabled-by-default feature flag. This violates the
requirement that new features be behind an off-by-default flag to preserve pre-change behavior
unless explicitly enabled.
Code

src/node/handler/PadMessageHandler.ts[R1004-1012]

+    // Only the original creator of the pad (revision 0 author) receives the
+    // deletion token, and only on their first arrival — subsequent visits get
+    // null because createDeletionTokenIfAbsent() only emits a plaintext token
+    // once. Readonly sessions never see it.
+    const isCreator =
+        !sessionInfo.readonly && sessionInfo.author === await pad.getRevisionAuthor(0);
+    const padDeletionToken = isCreator
+        ? await padDeletionManager.createDeletionTokenIfAbsent(sessionInfo.padId)
+        : null;
Evidence
PR Compliance ID 5 requires new functionality to be behind a feature flag and disabled by default.
The PR unconditionally generates and returns a deletion token on pad creation and surfaces it to
creator sessions via clientVars without any enabling flag, so the feature is on by default.

src/node/db/API.ts[520-546]
src/node/handler/PadMessageHandler.ts[1004-1012]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Deletion tokens are generated and surfaced by default (API response and UI modal) without a feature flag. Compliance requires new features to be behind a disabled-by-default flag, with the default path matching pre-change behavior.
## Issue Context
Current behavior:
- `createPad()` always returns `{deletionToken: ...}`
- Creator sessions always attempt token creation and receive `clientVars.padDeletionToken`
## Fix Focus Areas
- src/node/db/API.ts[520-546]
- src/node/handler/PadMessageHandler.ts[1004-1012]
- src/node/utils/Settings.ts[172-176]
- settings.json.template[480-486]
- settings.json.docker[483-489]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. HTTP API docs not updated 📘 Rule violation ⚙ Maintainability
Description
The PR changes the public HTTP API (createPad now returns deletionToken; deletePad accepts
optional deletionToken) but the HTTP API documentation still describes the old signatures/returns.
This can cause integrator confusion and violates the requirement to update docs in the same PR when
changing APIs.
Code

src/node/db/API.ts[R520-546]

 // create pad
 await getPadSafe(padID, false, text, authorId);
+  return {deletionToken: await padDeletionManager.createDeletionTokenIfAbsent(padID)};
};

/**
-deletePad(padID) deletes a pad
+deletePad(padID, [deletionToken]) deletes a pad

Example returns:

{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
+{code: 1, message:"invalid deletionToken", data: null}
@param {String} padID the id of the pad
+ @param {String} [deletionToken] recovery token issued by createPad
*/
-exports.deletePad = async (padID: string) => {
+exports.deletePad = async (padID: string, deletionToken?: string) => {
 const pad = await getPadSafe(padID, true);
+  // apikey-authenticated callers (no deletionToken supplied) are trusted.
+  // When a caller supplies a deletionToken, it must validate unless the
+  // instance has opted everyone in via allowPadDeletionByAllUsers.
+  if (deletionToken !== undefined && deletionToken !== '' &&
+      !settings.allowPadDeletionByAllUsers &&
+      !await padDeletionManager.isValidDeletionToken(padID, deletionToken)) {
+    throw new CustomError('invalid deletionToken', 'apierror');
+  }
Evidence
PR Compliance IDs 10 and 11 require API changes to be documented in the same PR. The code now
returns a deletionToken from createPad and adds an optional deletionToken parameter and error
to deletePad, but doc/api/http_api.md still documents createPad returning data: null and
deletePad(padID) with no token parameter.

src/node/db/API.ts[520-548]
src/node/handler/APIHandler.ts[53-56]
doc/api/http_api.md[519-592]
Best Practice: Repository guidelines
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The HTTP API docs are out of date relative to the changed endpoints:
- `createPad` now returns `data: {deletionToken: string|null}`
- `deletePad` now accepts optional `deletionToken` and may return `invalid deletionToken`
## Issue Context
The OpenAPI schema arg list was updated, but the human-readable HTTP API documentation (`doc/api/http_api.md`) still shows the previous signatures and example returns.
## Fix Focus Areas
- src/node/db/API.ts[520-548]
- src/node/handler/APIHandler.ts[53-56]
- doc/api/http_api.md[519-592]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Token creation race 🐞 Bug ≡ Correctness
Description
createDeletionTokenIfAbsent() uses a non-atomic read-then-write, so concurrent calls for the same
pad can each return different plaintext tokens and overwrite the stored hash, making at least one
returned token permanently invalid.
Code

src/node/db/PadDeletionManager.ts[R13-20]

+exports.createDeletionTokenIfAbsent = async (padId: string): Promise<string | null> => {
+  if (await DB.db.get(getDeletionTokenKey(padId)) != null) return null;
+  const deletionToken = randomString(32);
+  await DB.db.set(getDeletionTokenKey(padId), {
+    createdAt: Date.now(),
+    hash: hashDeletionToken(deletionToken).toString('hex'),
+  });
+  return deletionToken;
Evidence
The token is issued via DB.db.get() followed by DB.db.set() with no locking/CAS, so two
concurrent callers can both observe “absent” and then store different hashes. This function is
called during creator CLIENT_READY and also on HTTP API pad creation, making concurrent invocation
plausible (e.g., multiple creator tabs loading simultaneously).

src/node/db/PadDeletionManager.ts[13-20]
src/node/handler/PadMessageHandler.ts[1008-1012]
src/node/db/API.ts[508-524]
src/node/db/GroupManager.ts[140-173]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`createDeletionTokenIfAbsent()` does a check-then-set on the DB key. Under concurrency, two calls for the same `padId` can both return different plaintext tokens, but only the last stored hash will validate, causing at least one client to save an unusable recovery token.
### Issue Context
This function is called from multiple entry points (creator `CLIENT_READY`, `createPad`, `createGroupPad`). Even without multi-process, two creator tabs can race.
### Fix Focus Areas
- src/node/db/PadDeletionManager.ts[13-21]
- src/node/handler/PadMessageHandler.ts[1008-1012]
### Suggested fix
Implement a per-`padId` in-memory mutex/in-flight promise map around the token creation path:
- Re-check existence after acquiring the lock.
- Ensure only one call performs the `set` and returns the plaintext token; concurrent callers should reliably return `null`.
If Etherpad can run multi-process in your deployment, document this limitation or consider a DB-level atomic primitive if available.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
5. createPad docs contradict behavior 🐞 Bug ⚙ Maintainability ⭐ New
Description
The HTTP API docs claim createPad can return {deletionToken: null} when the pad already exists,
but the implementation throws padID does already exist and does not return success data. This will
mislead API consumers about idempotency and response shape.
Code

doc/api/http_api.md[R527-536]

+`data.deletionToken` is a one-shot recovery token tied to this pad. It is
+returned in plaintext on the first call for a given padID and is `null` on
+subsequent calls (the token itself is stored on the server as a sha256 hash).
+Pass it to **deletePad** (or the socket `PAD_DELETE` message) to delete the
+pad without the creator's author cookie.
+
*Example returns:*
-* `{code: 0, message:"ok", data: null}`
+* `{code: 0, message:"ok", data: {deletionToken: "…32-char random string…"}}`
+* `{code: 0, message:"ok", data: {deletionToken: null}}` — pad already existed
* `{code: 1, message:"padID does already exist", data: null}`
Evidence
The docs describe a successful (code 0) response for an already-existing pad, but the implementation
uses getPadSafe(..., shouldExist=false), which throws if the pad exists. Therefore that documented
response shape cannot occur.

doc/api/http_api.md[519-537]
src/node/db/API.ts[508-524]
src/node/db/API.ts[907-933]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`doc/api/http_api.md` documents a `{code: 0, ..., data: {deletionToken: null}}` outcome for `createPad` when the pad already exists, but the server code throws an error in that case.

### Issue Context
`createPad()` calls `getPadSafe(padID, false, ...)`, and `getPadSafe()` throws `padID does already exist` if the pad exists.

### Fix Focus Areas
- doc/api/http_api.md[519-537]
- src/node/db/API.ts[508-524]
- src/node/db/API.ts[907-933]

### Suggested change
- Either update the docs to remove/adjust the “pad already existed” success example, OR change `createPad` semantics to be idempotent (return code 0 with `{deletionToken: null}`) if that is the intended contract.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Delete token listeners persist 🐞 Bug ☼ Reliability
Description
The delete-by-token click handler adds persistent pad.socket.on('message') and
pad.socket.on('shout') listeners on every click and never removes them, so after a refused attempt
the page can accumulate handlers and later shouts can trigger multiple alert()s unrelated to
deletion.
Code

src/static/js/pad_editor.ts[R89-114]

+      // delete pad using a recovery token (second device / no creator cookie)
+      $('#delete-pad-token-submit').on('click', () => {
+        const token = String($('#delete-pad-token-input').val() || '').trim();
+        if (!token) return;
+        if (!window.confirm(html10n.get('pad.delete.confirm'))) return;
+
+        let handled = false;
+        pad.socket.on('message', (data: any) => {
+          if (data && data.disconnect === 'deleted') {
+            handled = true;
+            window.location.href = '/';
+          }
+        });
+        pad.socket.on('shout', (data: any) => {
+          handled = true;
+          const msg = data?.data?.payload?.message?.message;
+          if (msg) window.alert(msg);
+        });
+        pad.collabClient.sendMessage({
+          type: 'PAD_DELETE',
+          data: {padId: pad.getPadId(), deletionToken: token},
+        });
+        setTimeout(() => {
+          if (!handled) window.location.href = '/';
+        }, 5000);
+      });
Evidence
The new handler registers .on('message') and .on('shout') inside the click callback and does not
deregister them. If deletion is refused (shout path), the page stays loaded, so subsequent clicks
stack more listeners; existing listeners also alert() on any future shout payload because there is
no filtering or cleanup.

src/static/js/pad_editor.ts[89-114]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The delete-by-token flow attaches socket listeners on every click and never removes them, which can lead to accumulated handlers and repeated alerts/redirect behavior after multiple attempts.
### Issue Context
If the token is wrong, the server emits a shout and the page does not navigate away, so the user can retry and stack handlers.
### Fix Focus Areas
- src/static/js/pad_editor.ts[89-114]
### Suggested fix
- Use `.once()` instead of `.on()` for the temporary listeners.
- Or namespace and cleanup explicitly: `pad.socket.off('message.gdprDelete').on('message.gdprDelete', ...)` and similarly for `shout`, then remove them when handled or when the 5s timeout fires.
- Optionally filter the shout handler to only react to the specific refusal message/type used for pad deletion to avoid alerting on unrelated shouts.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments
7. Empty token skips validation 🐞 Bug ⚙ Maintainability
Description
API.deletePad() treats deletionToken === '' as if no token was supplied, skipping validation and
proceeding to delete, which makes the parameter semantics ambiguous and unsafe if this path is ever
reused for non-admin deletion.
Code

src/node/db/API.ts[R542-546]

+  if (deletionToken !== undefined && deletionToken !== '' &&
+      !settings.allowPadDeletionByAllUsers &&
+      !await padDeletionManager.isValidDeletionToken(padID, deletionToken)) {
+    throw new CustomError('invalid deletionToken', 'apierror');
+  }
Evidence
The validation guard explicitly excludes the empty string, but the API handler passes query fields
positionally, so callers can send deletionToken= and bypass token validation logic in this
function. Today the REST API is authenticated, which limits impact, but the behavior contradicts the
notion of “if deletionToken is provided, validate it.”

src/node/db/API.ts[537-546]
src/node/handler/APIHandler.ts[35-62]
src/node/handler/APIHandler.ts[214-219]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`deletePad(padID, deletionToken?)` skips validation when `deletionToken` is an empty string, which makes the behavior inconsistent and potentially risky if reused for token-based deletion.
### Issue Context
`isValidDeletionToken()` already considers `''` invalid; the API wrapper currently avoids calling it for `''`.
### Fix Focus Areas
- src/node/db/API.ts[537-546]
### Suggested fix
Change the condition to validate any provided token, including empty strings:
- Use `if (deletionToken != null && !settings.allowPadDeletionByAllUsers && !await isValidDeletionToken(...)) throw ...;`
This preserves the “no token supplied” bypass for authenticated admin callers (undefined) while making `''` correctly fail.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit a3d2011


🐞 Bugs (3) 📘 Rule violations (2) 📎 Requirement gaps (0)


Action required
1. Deletion token not feature-flagged 📘 Rule violation ☼ Reliability
Description
Pad deletion tokens (API response + creator modal) are enabled unconditionally, changing default
behavior rather than being gated behind a disabled-by-default feature flag. This violates the
requirement that new features be behind an off-by-default flag to preserve pre-change behavior
unless explicitly enabled.
Code

src/node/handler/PadMessageHandler.ts[R1004-1012]

+    // Only the original creator of the pad (revision 0 author) receives the
+    // deletion token, and only on their first arrival — subsequent visits get
+    // null because createDeletionTokenIfAbsent() only emits a plaintext token
+    // once. Readonly sessions never see it.
+    const isCreator =
+        !sessionInfo.readonly && sessionInfo.author === await pad.getRevisionAuthor(0);
+    const padDeletionToken = isCreator
+        ? await padDeletionManager.createDeletionTokenIfAbsent(sessionInfo.padId)
+        : null;
Evidence
PR Compliance ID 5 requires new functionality to be behind a feature flag and disabled by default.
The PR unconditionally generates and returns a deletion token on pad creation and surfaces it to
creator sessions via clientVars without any enabling flag, so the feature is on by default.

src/node/db/API.ts[520-546]
src/node/handler/PadMessageHandler.ts[1004-1012]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Deletion tokens are generated and surfaced by default (API response and UI modal) without a feature flag. Compliance requires new features to be behind a disabled-by-default flag, with the default path matching pre-change behavior.

## Issue Context
Current behavior:
- `createPad()` always returns `{deletionToken: ...}`
- Creator sessions always attempt token creation and receive `clientVars.padDeletionToken`

## Fix Focus Areas
- src/node/db/API.ts[520-546]
- src/node/handler/PadMessageHandler.ts[1004-1012]
- src/node/utils/Settings.ts[172-176]
- settings.json.template[480-486]
- settings.json.docker[483-489]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. HTTP API docs not updated 📘 Rule violation ⚙ Maintainability
Description
The PR changes the public HTTP API (createPad now returns deletionToken; deletePad accepts
optional deletionToken) but the HTTP API documentation still describes the old signatures/returns.
This can cause integrator confusion and violates the requirement to update docs in the same PR when
changing APIs.
Code

src/node/db/API.ts[R520-546]

  // create pad
  await getPadSafe(padID, false, text, authorId);
+  return {deletionToken: await padDeletionManager.createDeletionTokenIfAbsent(padID)};
};

/**
-deletePad(padID) deletes a pad
+deletePad(padID, [deletionToken]) deletes a pad

Example returns:

{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
+{code: 1, message:"invalid deletionToken", data: null}
 @param {String} padID the id of the pad
+ @param {String} [deletionToken] recovery token issued by createPad
*/
-exports.deletePad = async (padID: string) => {
+exports.deletePad = async (padID: string, deletionToken?: string) => {
  const pad = await getPadSafe(padID, true);
+  // apikey-authenticated callers (no deletionToken supplied) are trusted.
+  // When a caller supplies a deletionToken, it must validate unless the
+  // instance has opted everyone in via allowPadDeletionByAllUsers.
+  if (deletionToken !== undefined && deletionToken !== '' &&
+      !settings.allowPadDeletionByAllUsers &&
+      !await padDeletionManager.isValidDeletionToken(padID, deletionToken)) {
+    throw new CustomError('invalid deletionToken', 'apierror');
+  }
Evidence
PR Compliance IDs 10 and 11 require API changes to be documented in the same PR. The code now
returns a deletionToken from createPad and adds an optional deletionToken parameter and error
to deletePad, but doc/api/http_api.md still documents createPad returning data: null and
deletePad(padID) with no token parameter.

src/node/db/API.ts[520-548]
src/node/handler/APIHandler.ts[53-56]
doc/api/http_api.md[519-592]
Best Practice: Repository guidelines
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The HTTP API docs are out of date relative to the changed endpoints:
- `createPad` now returns `data: {deletionToken: string|null}`
- `deletePad` now accepts optional `deletionToken` and may return `invalid deletionToken`

## Issue Context
The OpenAPI schema arg list was updated, but the human-readable HTTP API documentation (`doc/api/http_api.md`) still shows the previous signatures and example returns.

## Fix Focus Areas
- src/node/db/API.ts[520-548]
- src/node/handler/APIHandler.ts[53-56]
- doc/api/http_api.md[519-592]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Token creation race 🐞 Bug ≡ Correctness
Description
createDeletionTokenIfAbsent() uses a non-atomic read-then-write, so concurrent calls for the same
pad can each return different plaintext tokens and overwrite the stored hash, making at least one
returned token permanently invalid.
Code

src/node/db/PadDeletionManager.ts[R13-20]

+exports.createDeletionTokenIfAbsent = async (padId: string): Promise<string | null> => {
+  if (await DB.db.get(getDeletionTokenKey(padId)) != null) return null;
+  const deletionToken = randomString(32);
+  await DB.db.set(getDele...

Comment on lines +1004 to +1012
// Only the original creator of the pad (revision 0 author) receives the
// deletion token, and only on their first arrival — subsequent visits get
// null because createDeletionTokenIfAbsent() only emits a plaintext token
// once. Readonly sessions never see it.
const isCreator =
!sessionInfo.readonly && sessionInfo.author === await pad.getRevisionAuthor(0);
const padDeletionToken = isCreator
? await padDeletionManager.createDeletionTokenIfAbsent(sessionInfo.padId)
: null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Deletion token not feature-flagged 📘 Rule violation ☼ Reliability

Pad deletion tokens (API response + creator modal) are enabled unconditionally, changing default
behavior rather than being gated behind a disabled-by-default feature flag. This violates the
requirement that new features be behind an off-by-default flag to preserve pre-change behavior
unless explicitly enabled.
Agent Prompt
## Issue description
Deletion tokens are generated and surfaced by default (API response and UI modal) without a feature flag. Compliance requires new features to be behind a disabled-by-default flag, with the default path matching pre-change behavior.

## Issue Context
Current behavior:
- `createPad()` always returns `{deletionToken: ...}`
- Creator sessions always attempt token creation and receive `clientVars.padDeletionToken`

## Fix Focus Areas
- src/node/db/API.ts[520-546]
- src/node/handler/PadMessageHandler.ts[1004-1012]
- src/node/utils/Settings.ts[172-176]
- settings.json.template[480-486]
- settings.json.docker[483-489]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/node/db/API.ts
Comment on lines 520 to +546

// create pad
await getPadSafe(padID, false, text, authorId);
return {deletionToken: await padDeletionManager.createDeletionTokenIfAbsent(padID)};
};

/**
deletePad(padID) deletes a pad
deletePad(padID, [deletionToken]) deletes a pad

Example returns:

{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
{code: 1, message:"invalid deletionToken", data: null}
@param {String} padID the id of the pad
@param {String} [deletionToken] recovery token issued by createPad
*/
exports.deletePad = async (padID: string) => {
exports.deletePad = async (padID: string, deletionToken?: string) => {
const pad = await getPadSafe(padID, true);
// apikey-authenticated callers (no deletionToken supplied) are trusted.
// When a caller supplies a deletionToken, it must validate unless the
// instance has opted everyone in via allowPadDeletionByAllUsers.
if (deletionToken !== undefined && deletionToken !== '' &&
!settings.allowPadDeletionByAllUsers &&
!await padDeletionManager.isValidDeletionToken(padID, deletionToken)) {
throw new CustomError('invalid deletionToken', 'apierror');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Http api docs not updated 📘 Rule violation ⚙ Maintainability

The PR changes the public HTTP API (createPad now returns deletionToken; deletePad accepts
optional deletionToken) but the HTTP API documentation still describes the old signatures/returns.
This can cause integrator confusion and violates the requirement to update docs in the same PR when
changing APIs.
Agent Prompt
## Issue description
The HTTP API docs are out of date relative to the changed endpoints:
- `createPad` now returns `data: {deletionToken: string|null}`
- `deletePad` now accepts optional `deletionToken` and may return `invalid deletionToken`

## Issue Context
The OpenAPI schema arg list was updated, but the human-readable HTTP API documentation (`doc/api/http_api.md`) still shows the previous signatures and example returns.

## Fix Focus Areas
- src/node/db/API.ts[520-548]
- src/node/handler/APIHandler.ts[53-56]
- doc/api/http_api.md[519-592]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/node/db/PadDeletionManager.ts Outdated
Comment on lines +13 to +20
exports.createDeletionTokenIfAbsent = async (padId: string): Promise<string | null> => {
if (await DB.db.get(getDeletionTokenKey(padId)) != null) return null;
const deletionToken = randomString(32);
await DB.db.set(getDeletionTokenKey(padId), {
createdAt: Date.now(),
hash: hashDeletionToken(deletionToken).toString('hex'),
});
return deletionToken;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Token creation race 🐞 Bug ≡ Correctness

createDeletionTokenIfAbsent() uses a non-atomic read-then-write, so concurrent calls for the same
pad can each return different plaintext tokens and overwrite the stored hash, making at least one
returned token permanently invalid.
Agent Prompt
### Issue description
`createDeletionTokenIfAbsent()` does a check-then-set on the DB key. Under concurrency, two calls for the same `padId` can both return different plaintext tokens, but only the last stored hash will validate, causing at least one client to save an unusable recovery token.

### Issue Context
This function is called from multiple entry points (creator `CLIENT_READY`, `createPad`, `createGroupPad`). Even without multi-process, two creator tabs can race.

### Fix Focus Areas
- src/node/db/PadDeletionManager.ts[13-21]
- src/node/handler/PadMessageHandler.ts[1008-1012]

### Suggested fix
Implement a per-`padId` in-memory mutex/in-flight promise map around the token creation path:
- Re-check existence after acquiring the lock.
- Ensure only one call performs the `set` and returns the plaintext token; concurrent callers should reliably return `null`.
If Etherpad can run multi-process in your deployment, document this limitation or consider a DB-level atomic primitive if available.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@SamTV12345
Copy link
Copy Markdown
Member

image The button is kind of off. It looks more like a text than something I can click on.

@JohnMcLear
Copy link
Copy Markdown
Member Author

Yea sorry this needs to be draft.

@JohnMcLear JohnMcLear marked this pull request as draft April 27, 2026 09:54
@JohnMcLear JohnMcLear marked this pull request as ready for review May 1, 2026 09:20
@qodo-code-review
Copy link
Copy Markdown

ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 1, 2026

Persistent review updated to latest commit baa11b5

@JohnMcLear
Copy link
Copy Markdown
Member Author

JohnMcLear commented May 1, 2026

Changed some strings, based, fixed some practical and some visual bugs.. Should be pretty much ready for you to review again :)

JohnMcLear and others added 8 commits May 1, 2026 10:27
First of five GDPR PRs tracked in #6701. PR1 covers deletion controls:
one-time deletion token, allowPadDeletionByAllUsers flag, authorisation
matrix for handlePadDelete and the REST deletePad endpoint, a single
token-display modal for browser pad creators, and test coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 TDD-structured tasks covering PadDeletionManager unit tests, socket
+ REST three-way auth, clientVars wiring, one-time token modal,
delete-with-token UI, Playwright coverage, and PR handoff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PadDeletionManager stores a sha256-hashed per-pad deletion token and
verifies it with timing-safe comparison. createPad / createGroupPad
return the plaintext token once on first creation, and Pad.remove()
cleans it up. Gated behind the new allowPadDeletionByAllUsers flag
which defaults to false to preserve existing behaviour.

Part of #6701 (GDPR PR1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Capturing DB.db at module-load time was null until DB.init() ran, which
broke importing the module outside a live server (including from the
test runner). Switch to DB.db.* at call time and add unit tests
exercising create/verify/remove plus timing-safe comparison.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Creator cookie → valid deletion token → allowPadDeletionByAllUsers flag.
Anyone else still gets the existing refusal shout.
Comment thread src/node/db/API.ts Outdated

// create pad
await getPadSafe(padID, false, text, authorId);
return {deletionToken: await padDeletionManager.createDeletionTokenIfAbsent(padID)};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. createpad response shape changed 📘 Rule violation ⚙ Maintainability

createPad now returns data: {deletionToken: ...} (or {deletionToken: null}) instead of `data:
null`, changing the response schema for existing API consumers. This is a potentially breaking
public API change that should be avoided or versioned/opt-in and clearly documented as breaking.
Agent Prompt
## Issue description
The `createPad` API response `data` changes from `null` to an object containing `deletionToken`, which can break clients that assume `data` is always `null` for this endpoint.

## Issue Context
This is a public HTTP API behavior change. To comply, preserve backward compatibility (or introduce a versioned/opt-in path).

## Fix Focus Areas
- src/node/db/API.ts[518-524]
- doc/api/http_api.md[519-536]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

JohnMcLear and others added 11 commits May 1, 2026 10:40
Revision-0 author on their first CLIENT_READY visit receives the
plaintext token; all subsequent CLIENT_READYs receive null because
createDeletionTokenIfAbsent is idempotent. Readonly sessions and any
other user never see the token.
The token modal introduced in PR1 blocks clicks for every Playwright
test that creates a new pad via the shared helper. Add a one-line
dismissal so unrelated tests keep passing, and have the deletion-token
spec navigate inline via newPadKeepingModal() when it needs the modal
open to capture the token.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clicking the ack button transferred focus out of the pad iframe, which
made subsequent keyboard-driven tests (Tab / Enter) silently miss the
editor. Swap the click for a page.evaluate() that hides the modal and
nulls clientVars.padDeletionToken directly, leaving focus where it was.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Qodo review:
- createDeletionTokenIfAbsent() was a non-atomic read-then-write. Two
  concurrent callers for the same pad could both return different
  plaintext tokens while only the later hash was stored, leaving the
  first caller with an unusable recovery token. Serialise per-pad via a
  Promise chain and add a regression test that fires 8 concurrent
  calls and asserts exactly one plaintext is emitted and validates.
- doc/api/http_api.md now documents createPad returning deletionToken
  and deletePad accepting the optional deletionToken parameter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The rebase onto develop placed the delete-pad-with-token details inside
the pad-settings-section conditional, which is only rendered when
enablePadWideSettings is true AND the section is toggled visible.
Second-device recovery (typing the captured token on a fresh browser)
must work without pad-wide settings enabled, so move the details out
to sit alongside the existing pad_deletion_token.spec.ts expectations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…1y/i18n

- PadMessageHandler: a supplied deletion token must validate; do not fall
  back to the creator-cookie path when the token is wrong (was deleting
  the pad anyway when the creator pasted a wrong token into the field).
- Skip token issuance + UI when requireAuthentication is on (creator
  identity is stable, recovery token is redundant noise).
- Server emits messageKey instead of hardcoded English; both shout
  handlers (inline alert and global gritter) localize via html10n.
- Suppress the global "Admin message" gritter for pad.deletionToken.*
  shouts to avoid the "Admin message: undefined" duplicate.
- Token-modal a11y: role=dialog, aria-modal, aria-labelledby/describedby,
  visually-hidden label on the token input, aria-live on Copy, focus to
  the token input on open and restore on dismiss.
- Style the "Delete Pad with Token" disclosure to match the Delete pad
  button; align the Copy/value row; pad the disclosure label.

Tests: Playwright now covers the creator-with-wrong-token path, asserts
no "Admin message" / "undefined" gritter on denial; backend API test
covers requireAuthentication suppressing the token.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: end-to-end install (windows-latest)

Failed stage: Smoke test - start Etherpad and curl /api [❌]

Failed test name: ""

Failure summary:

The action failed in the Windows startup/health-check step because the script never observed a
successful response from http://localhost:9001/api within 90 seconds and exited with code 1.

Evidence:
- The PowerShell loop repeatedly ran Invoke-WebRequest ... -Uri http://localhost:9001/api
and, on failure, eventually executed the timeout branch (Etherpad did not start within 90s... then
exit 1).
- The captured Etherpad stdout shows the server did start and listen on port 9001 (HTTP
server listening for connections, Etherpad is running), implying the specific health-check URL
likely did not return a success status (e.g., /api returning non-2xx such as 404, which causes
Invoke-WebRequest to throw) even though the process was up.

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

323:  �[36;1mif (-not (Test-Path "$env:ETHERPAD_DIR\node_modules"))              { throw 'node_modules missing' }�[0m
324:  �[36;1mif (-not (Test-Path "$env:ETHERPAD_DIR\src\node_modules"))          { throw 'src/node_modules missing' }�[0m
325:  �[36;1mif (-not (Test-Path "$env:ETHERPAD_DIR\src\templates\admin\index.html")) { throw 'admin SPA missing' }�[0m
326:  shell: C:\Program Files\PowerShell\7\pwsh.EXE -command ". '{0}'"
327:  env:
328:  ETHERPAD_DIR: D:\a\_temp\etherpad-installer-test
329:  ##[endgroup]
330:  ##[group]Run Push-Location $env:ETHERPAD_DIR
331:  �[36;1mPush-Location $env:ETHERPAD_DIR�[0m
332:  �[36;1m$logPath = Join-Path $env:RUNNER_TEMP 'etherpad.log'�[0m
333:  �[36;1m# pnpm on Windows is a .cmd shim, which Start-Process can't run�[0m
334:  �[36;1m# directly — wrap it in cmd.exe.�[0m
335:  �[36;1m$proc = Start-Process -FilePath cmd.exe `�[0m
336:  �[36;1m                      -ArgumentList '/c','pnpm','run','prod' `�[0m
337:  �[36;1m                      -RedirectStandardOutput $logPath `�[0m
338:  �[36;1m                      -RedirectStandardError "$logPath.err" `�[0m
339:  �[36;1m                      -PassThru -NoNewWindow�[0m
340:  �[36;1m$ok = $false�[0m
341:  �[36;1mfor ($i = 1; $i -le 90; $i++) {�[0m
342:  �[36;1m  try {�[0m
343:  �[36;1m    Invoke-WebRequest -UseBasicParsing -Uri http://localhost:9001/api -TimeoutSec 2 | Out-Null�[0m
344:  �[36;1m    Write-Host "Etherpad answered on /api after ${i}s"�[0m
345:  �[36;1m    $ok = $true�[0m
346:  �[36;1m    break�[0m
347:  �[36;1m  } catch { Start-Sleep -Seconds 1 }�[0m
348:  �[36;1m}�[0m
349:  �[36;1mif (-not $ok) {�[0m
350:  �[36;1m  Write-Host 'Etherpad did not start within 90s. Last 200 lines of log (stdout):'�[0m
351:  �[36;1m  if (Test-Path $logPath) { Get-Content $logPath -Tail 200 }�[0m
352:  �[36;1m  Write-Host 'Last 200 lines of stderr:'�[0m
353:  �[36;1m  if (Test-Path "$logPath.err") { Get-Content "$logPath.err" -Tail 200 }�[0m
354:  �[36;1m  Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue�[0m
355:  �[36;1m  Pop-Location�[0m
356:  �[36;1m  exit 1�[0m
357:  �[36;1m}�[0m
358:  �[36;1mStop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue�[0m
359:  �[36;1mPop-Location�[0m
...

376:  �[32m[2026-05-01T09:44:06.042] [INFO] settings - �[39mString used for versioning assets: 00ce505f
377:  �[32m[2026-05-01T09:44:06.420] [INFO] server - �[39mStarting Etherpad...
378:  �[32m[2026-05-01T09:44:06.462] [INFO] plugins - �[39mcheck installed plugins for migration
379:  �[32m[2026-05-01T09:44:06.466] [INFO] plugins - �[39mstart migration of plugins in node_modules
380:  �[32m[2026-05-01T09:44:06.663] [INFO] plugins - �[39mpnpm --version: 10.33.2
381:  �[32m[2026-05-01T09:44:07.020] [INFO] plugins - �[39mLoading plugin ep_etherpad-lite...
382:  �[32m[2026-05-01T09:44:07.021] [INFO] plugins - �[39mLoaded 1 plugins
383:  �[32m[2026-05-01T09:44:09.822] [INFO] server - �[39mInstalled plugins: 
384:  �[32m[2026-05-01T09:44:09.824] [INFO] settings - �[39mReport bugs at https://github.com/ether/etherpad/issues
385:  �[32m[2026-05-01T09:44:09.825] [INFO] settings - �[39mYour Etherpad version is 2.7.2 (703c5a3)
386:  �[32m[2026-05-01T09:44:10.802] [INFO] http - �[39mHTTP server listening for connections
387:  �[32m[2026-05-01T09:44:10.803] [INFO] settings - �[39mYou can access your Etherpad instance at http://0.0.0.0:9001/
388:  �[33m[2026-05-01T09:44:10.803] [WARN] settings - �[39mAdmin username and password not set in settings.json. To access admin please uncomment and edit "users" in settings.json
389:  �[32m[2026-05-01T09:44:10.804] [INFO] server - �[39mEtherpad is running
390:  Last 200 lines of stderr:
391:  ##[error]Process completed with exit code 1.
392:  Post job cleanup.

@JohnMcLear
Copy link
Copy Markdown
Member Author

/review

@qodo-code-review
Copy link
Copy Markdown

ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 1, 2026

Persistent review updated to latest commit 703c5a3

Comment on lines +268 to +278
const suppliedToken = padDeleteMessage.data.deletionToken;
const tokenSupplied = typeof suppliedToken === 'string' && suppliedToken !== '';
const tokenOk = tokenSupplied &&
await padDeletionManager.isValidDeletionToken(padId, suppliedToken);
// When a token is supplied it must validate. We deliberately do NOT fall
// back to the creator-cookie path, otherwise a creator pasting a wrong
// recovery token into the disclosure field would still succeed — masking a
// typo and contradicting the UI.
const creatorOk = !tokenSupplied && isCreator;
const flagOk = !tokenSupplied && !isCreator && settings.allowPadDeletionByAllUsers;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Deletion flag unusable in ui 🐞 Bug ≡ Correctness

allowPadDeletionByAllUsers is only honored when no deletionToken is supplied, but the only
non-creator UI control always supplies a token, so enabling the flag still doesn’t let regular
editors delete via the UI. This is also inconsistent with the REST deletePad behavior, which
bypasses token checks when the flag is enabled even if a bogus token is provided.
Agent Prompt
### Issue description
`allowPadDeletionByAllUsers` is intended to allow any editor to delete without needing the one-time token, but the socket handler only applies the flag when *no token is supplied*. The UI’s non-creator delete path always supplies a token, so non-creators cannot delete via UI even when the flag is enabled.

### Issue Context
- Socket handler: `flagOk` is gated on `!tokenSupplied`.
- UI token-delete always sends `deletionToken`, so `tokenSupplied` is always true.
- Non-creators don’t have a UI path to send `PAD_DELETE` without a token because `canEditPadSettings` is creator-only.
- REST deletePad bypasses validation when the flag is enabled, even if a bogus token is supplied, so socket/REST are inconsistent.

### Fix Focus Areas
- src/node/handler/PadMessageHandler.ts[258-302]
- src/static/js/pad_editor.ts[140-200]
- src/node/handler/PadMessageHandler.ts[1123-1165]
- src/templates/pad.html[256-264]
- src/node/db/API.ts[543-552]

### Suggested approach
Choose one consistent policy and implement it across socket + UI:
1) **Server-side simplest:** If `settings.allowPadDeletionByAllUsers` is true, allow deletion regardless of `deletionToken` presence/validity (ignore token like REST does), and only use token validation when the flag is false.
2) **UI-side alternative:** Pass `allowPadDeletionByAllUsers` (or a `canDeletePad` boolean) via `clientVars` and, when enabled, have the UI send `PAD_DELETE` without `deletionToken` (or omit it) so the existing `flagOk` branch is reached.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +170 to +173
return {
padID,
deletionToken: await padDeletionManager.createDeletionTokenIfAbsent(padID),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Group pads leak deletion token 🐞 Bug ⛨ Security

createGroupPad always creates/returns a deletion token, even when settings.requireAuthentication is
enabled, contradicting createPad’s explicit decision to suppress token issuance in that mode. This
reintroduces the “extra surface to leak” for group pads and creates inconsistent behavior between
createPad and createGroupPad.
Agent Prompt
### Issue description
Group-pad creation (`createGroupPad`) always issues and returns a plaintext deletion token, even when `settings.requireAuthentication` is enabled. This contradicts `createPad`, which explicitly avoids issuing a token in that mode to reduce credential leakage surface.

### Issue Context
- `API.createPad` returns `deletionToken: null` when `settings.requireAuthentication` is true.
- `API.createGroupPad` is currently a direct export of `GroupManager.createGroupPad`, which always calls `createDeletionTokenIfAbsent()` and returns the plaintext.

### Fix Focus Areas
- src/node/db/GroupManager.ts[140-173]
- src/node/db/API.ts[46-52]
- src/node/db/API.ts[508-529]

### Suggested fix
Mirror `createPad` behavior:
- Import `settings` into `GroupManager.ts`.
- If `settings.requireAuthentication` is true, return `{padID, deletionToken: null}` and **do not** call `createDeletionTokenIfAbsent`.
- Keep the return type consistent (`string | null`) and update any related docs/tests if necessary.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@JohnMcLear JohnMcLear merged commit 5e8704f into develop May 1, 2026
37 of 46 checks passed
@JohnMcLear JohnMcLear deleted the feat-gdpr-pad-deletion branch May 1, 2026 12:50
JohnMcLear added a commit that referenced this pull request May 1, 2026
PR #7546 added a relative link in `doc/privacy.md` pointing to
`../docs/superpowers/specs/2026-04-18-gdpr-pr1-deletion-controls-design.md`,
which lives outside vitepress's `doc/` source root. VitePress reports
it as a dead link and the docs deploy on develop fails:

    (!) Found dead link
        ./../docs/superpowers/specs/2026-04-18-gdpr-pr1-deletion-controls-design
        in file /home/runner/work/etherpad/etherpad/doc/privacy.md
    Error: 1 dead link(s) found.

Point the link at the file on GitHub instead so the published site
resolves it and readers can still find the spec.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JohnMcLear added a commit that referenced this pull request May 1, 2026
…theft (#7643)

PR #7546 added a one-time pad-deletion-token modal that opens via the
clientVars handshake on creator sessions and synchronously focuses its
input through setTimeout(0). `goToNewPad`'s previous mitigation hid the
modal element after `waitForEditorReady`, but the editor iframe
attaches before clientVars arrives, so the hide runs against a still-
hidden modal, short-circuits, and the modal opens later mid-test —
stealing focus and dropping the next Enter / Tab. Visible on develop
in `enter.spec.ts:33` and `indentation.spec.ts:9` across all four
Playwright jobs (run 25214868650).

Intercept `clientVars` assignment via `page.addInitScript` and null out
`padDeletionToken` before `pad.ts`'s `showDeletionTokenModalIfPresent`
can read it, so the modal-show short-circuits at the source. The
deletion-token spec navigates inline with `page.goto` and does not
call this helper, so its modal still appears.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JohnMcLear added a commit that referenced this pull request May 1, 2026
* ci(docs): build on PRs and pin Node 22 (Qodo follow-up to #7640)

Qodo flagged two reliability gaps on the oxc-minify fix that landed in
#7640:

  1. The Deploy Docs to GitHub Pages workflow only ran on push to
     develop, so a PR that broke `pnpm run docs:build` was not caught
     until after merge — exactly how the dead-link regression in #7546
     escaped. Add a pull_request trigger that runs the same build but
     skips the deploy/upload steps via `if: github.event_name ==
     'push'`. Also include the workflow file itself in the path filter
     so changes to it are exercised on PR.
  2. oxc-minify@0.128.0 requires Node ^20.19.0 || >=22.12.0, but the
     workflow did not pin Node and the repo declared engines.node
     >=22.0.0 with engineStrict: true — a runner image (or local dev)
     on Node 22.0–22.11 would refuse to install. Pin Node 22 in the
     docs workflow with actions/setup-node@v6 (matching the rest of
     CI), and bump engines.node to >=22.12.0 so the project's
     engineStrict gate matches the actual minimum.

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

* ci(docs): split build and deploy so PR runs do not hit pages env protection

The previous attempt put `if: github.event_name == 'push'` on individual
deploy steps but kept the single job's `environment: github-pages`
binding. Environment protection rules reject any non-develop ref
(including `refs/pull/N/merge`), so the runner failed the entire job
at creation time before any step could execute:

    Branch "refs/pull/7645/merge" is not allowed to deploy to
    github-pages due to environment protection rules.

Split into two jobs: `build` runs on every trigger (PR + push) and
uploads the artifact only on push, `deploy` depends on `build`,
runs only on push, and is the only job bound to the github-pages
environment. Standard GHA pages-deploy pattern; PR builds never
attempt to enter the protected environment.

* docs: align Node minimum references with bumped engines.node (Qodo round 2 on #7645)

Qodo flagged that engines.node moved from >=22.0.0 to >=22.12.0 in
this PR but documentation still claimed the old requirement. Sync the
three places that pinned a specific minimum:

  - README.md installation requirements (>= 22 → >= 22.12)
  - doc/npm-trusted-publishing.md publish prerequisites
    (>=22.0.0 → >=22.12.0, with oxc-minify cited as the driver)
  - CHANGELOG.md 2.7.3 breaking-changes entry (22 → 22.12, with the
    same oxc-minify justification)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants