Skip to content

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

Open
JohnMcLear wants to merge 16 commits intodevelopfrom
feat-gdpr-pad-deletion
Open

feat(gdpr): pad deletion controls (PR1 of #6701)#7546
JohnMcLear wants to merge 16 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

JohnMcLear and others added 14 commits April 18, 2026 17:54
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.
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.
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

feat(gdpr): pad deletion controls with one-time tokens and recovery flow

✨ Enhancement

Grey Divider

Walkthroughs

Description
• One-time sha256-hashed deletion token, returned plaintext once on pad creation
• Three-way authorization: creator cookie, valid token, or allowPadDeletionByAllUsers flag
• Browser creators see token modal and can delete via recovery-token field in settings
• Comprehensive backend and frontend test coverage with Playwright and Mocha specs
Diagram
flowchart LR
  CreatePad["createPad/createGroupPad"] -->|generates token| PDM["PadDeletionManager"]
  PDM -->|stores hash| DB["Database"]
  PDM -->|returns plaintext| API["API Response"]
  API -->|browser only| ClientVars["clientVars.padDeletionToken"]
  ClientVars -->|creator session| Modal["Token Modal"]
  Modal -->|acknowledged| Settings["Settings Popup"]
  Settings -->|delete-with-token| PAD_DELETE["PAD_DELETE Message"]
  PAD_DELETE -->|three-way auth| Handler["handlePadDelete"]
  Handler -->|creator OR token OR flag| Remove["Pad.remove"]
  Remove -->|cleanup| PDM
Loading

Grey Divider

File Changes

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

New module for token creation, verification, and removal

src/node/db/PadDeletionManager.ts


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

Return deletion token from createPad, validate token on deletePad

src/node/db/API.ts


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

Return deletion token from createGroupPad

src/node/db/GroupManager.ts


View more (17)
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, thread token to clientVars

src/node/handler/PadMessageHandler.ts


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

Advertise optional deletionToken parameter in REST 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/-1

Add optional padDeletionToken to ClientVarPayload and PadDeleteMessage

src/static/js/types/SocketIOMessage.ts


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

Show token modal once on creator session, clear after acknowledgement

src/static/js/pad.ts


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

Wire delete-by-token input to send PAD_DELETE with recovery token

src/static/js/pad_editor.ts


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

Add token modal markup and delete-with-token disclosure UI

src/templates/pad.html


12. src/static/skins/colibris/src/components/popup.css ✨ Enhancement +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 flow

src/locales/en.json


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

Unit tests for token creation, verification, and removal

src/tests/backend/specs/padDeletionManager.ts


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

REST API tests covering authorization matrix and token validation

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


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

Playwright tests for modal display, token-based deletion, and error cases

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


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

Add allowPadDeletionByAllUsers configuration option

settings.json.template


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

Add allowPadDeletionByAllUsers configuration option

settings.json.docker


19. 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


20. 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


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 (3) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider


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(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

4. 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

5. 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

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

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 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

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>
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