Conversation
Enable Google OAuth2 login for self-hosted DocuSeal when GOOGLE_CLIENT_ID env var is present. Adds GitHub Actions workflow to build amd64 images and push to Google Artifact Registry via Workload Identity Federation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove all "Unlock with DocuSeal Pro" placeholder gates from views - Enable previously gated features: editor/viewer roles, email reminders, reply-to fields, esign preferences, SMTP security options - Remove enterprise path blocking from errors controller - Remove Rollbar error tracking (JS client, meta tags, all Ruby calls) and replace with Rails.logger equivalents - Gut newsletter and enquiries phone-home controllers - Gut console redirect controller and remove upgrade/plans links - Remove "Powered by DocuSeal" attribution and demo signup links - Remove pricing links from Vue components (fields, formulas, conditions) - Remove upgrade button from navbar and Plans/Pro badge from settings - Clean up docuseal.rb constants (NEWSLETTER_URL, ENQUIRIES_URL) - Remove docuseal.com domain handling from application controller Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add COMPANY_LOGO_URL_KEY to AccountConfig model - Add logo URL to PersonalizationSettingsController ALLOWED_KEYS - Implement logo URL form in personalization settings with preview - Display custom logo in signing form banner when configured - Display custom logo in submission detail view when configured - Falls back to default DocuSeal logo when no URL is set Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make omniauth_callbacks route conditional on GOOGLE_CLIENT_ID presence to match the User model's conditional devise :omniauthable. Fix erblint offenses: self-closing img tags, rescue modifier, and safe navigation chain length. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…etry Remove Pro feature gates, subscription code, and Rollbar telemetry
Use only short SHA and semver tags for immutable image references. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Every outgoing webhook request is now signed with HMAC-SHA256 using a per-webhook signing key. Receivers can verify authenticity via three new headers: X-Webhook-Signature (sha256=<digest>), X-Webhook-Timestamp, and X-Webhook-Request-Id. Signing keys are auto-generated for new webhooks and lazily provisioned for existing ones. The webhook secret UI now displays the signing key with a verification example and regenerate option. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace DocuSeal orange quill logo with Kencove fence-post icon in dark blue (#1E3A5F). Update product name from DocuSeal to Kencove eSign. Applied across shared logo partial, public logo.svg, Vue logo component, and navbar title. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire up the existing SMS plumbing with actual Twilio delivery: SendSms module, SendSubmitterInvitationSmsJob, SMS settings form, re-send SMS button, and automatic dispatch alongside email in send_signature_requests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Brings in 54 upstream commits including: - v2.3.4: SQL wildcard injection fix, CanCan authorization on downloads, 2FA refactoring, readonly field validation, credential hiding - v2.3.5: Rich text/markdown email editor, conditional signing party invitation, radio select in formula fields, Cloudflare R2 fix Conflict resolutions: - submissions_download_controller.rb: Kept upstream CanCan authorization check + Kencove's Rails.logger (instead of Rollbar) - Gemfile.lock: Re-bundled to include both upstream deps and Kencove's omniauth/oauth2 gems Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- SendSms: add open_timeout (8s) and read_timeout (15s) matching webhook config, return parsed Twilio JSON on success for SID capture - SendSubmitterInvitationSmsJob: disable Sidekiq's 25 blind retries, implement manual 5-attempt exponential backoff with error logging, store Twilio SID in SubmissionEvent.data on success - RateLimit: switch STORE from per-process MemoryStore to Redis-backed cache (falls back to MemoryStore if REDIS_URL not set or unavailable) - Add specs for SendSms, SendSubmitterInvitationSmsJob, and RateLimit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Default withFormula and withConditions props to true and remove Pro-only alert gates so template editors can create field conditions and formulas without requiring a Pro license. The runtime already evaluates both in CE — this just exposes the builder UI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…llback tests - Add test for Faraday::ConnectionFailed retry in SMS job spec (the job rescues this error but only Faraday::TimeoutError was previously tested) - Add STORE describe block in rate_limit_spec to verify MemoryStore fallback when REDIS_URL is absent, rescue behavior on Redis errors, and that the store supports increment for rate limiting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…NDING option Replace all hardcoded "DocuSeal" references in views and PDF generation with Docuseal.product_name for consistent branding. Add REMOVE_BRANDING env var to conditionally hide "powered by" footer and email attribution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use do...end for multi-line expect blocks and modifier if for single-line conditionals. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use the computed `value` and `dataValue` variables instead of raw `event.target.value` and `this.dataset.value` when setting the disabled state on hidden inputs. For checkboxes, event.target.value returns the HTML value attribute (e.g. "true") rather than the checked state, causing the disabled toggle to always evaluate incorrectly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Unlock conditions and formula UI in CE
…limit Add SMS error handling, retry logic, and Redis-backed rate limiting
Default to 'Kencove eSign' but allow override via PRODUCT_NAME environment variable for other deployments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rebrand to Kencove eSign with dynamic product name
There was a problem hiding this comment.
Actionable comments posted: 19
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/controllers/submit_form_values_controller.rb (1)
20-23:⚠️ Potential issue | 🟡 MinorInvalid
head: :okoption inrender json:.The
head:option is not valid forrender json:. Theheadmethod is a separate controller method that sends a response with only headers. This option will likely be ignored or cause unexpected behavior.🐛 Proposed fix
render json: { value:, attachment: attachment&.as_json(only: %i[uuid created_at], methods: %i[url filename content_type]) - }, head: :ok + }Or if you want to explicitly set the status:
render json: { value:, attachment: attachment&.as_json(only: %i[uuid created_at], methods: %i[url filename content_type]) - }, head: :ok + }, status: :ok🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/controllers/submit_form_values_controller.rb` around lines 20 - 23, The render call is using an invalid head: :ok option; update the controller to remove the head: :ok from the render json: call and either (a) replace it with status: :ok on the render invocation (render json: { value:, attachment: attachment&.as_json(...) }, status: :ok) or (b) remove the option and call head :ok separately after any needed processing; target the render json invocation that builds the response using value and attachment&.as_json(...).
♻️ Duplicate comments (2)
config/locales/i18n.yml (2)
650-651:⚠️ Potential issue | 🟠 MajorRemove duplicated
re_send_sms/send_smskeys.Line 650 and Line 651 redeclare keys already defined at Line 417-418. This is a YAML key-collision and is currently reported by YAMLlint.
🧹 Proposed fix
- re_send_sms: Re-send SMS - send_sms: Send SMS🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@config/locales/i18n.yml` around lines 650 - 651, There are duplicate YAML keys re_send_sms and send_sms; remove the redundant declarations so each key is defined only once (keep the original definitions and delete the later re_send_sms/send_sms entries), and scan the locale file for any other repeated keys to prevent YAML key-collisions (look for the symbols re_send_sms and send_sms to locate the duplicates).
356-357:⚠️ Potential issue | 🟠 MajorDuplicate locale key still present (
sms_has_been_sent).Line 356 duplicates
sms_has_been_sentalready defined at Line 595. This can silently override values and break YAML linting/parsing expectations.🧹 Proposed fix
- sms_has_been_sent: SMS has been sent. sms_has_been_sent_already: SMS has been sent already.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@config/locales/i18n.yml` around lines 356 - 357, There is a duplicate locale key sms_has_been_sent (also defined elsewhere) which can override or break YAML parsing; remove or consolidate the duplicate by deleting the redundant sms_has_been_sent entry in this block (or merge its value into the canonical sms_has_been_sent definition), and keep only one authoritative sms_has_been_sent key; also verify sms_has_been_sent_already is intentional and not a duplicate of another similar key.
🧹 Nitpick comments (11)
app/javascript/submission_form/invite_form.vue (1)
114-118: Consider spreadingfetchOptionsbefore explicit properties for defensive coding.The current spread order allows
fetchOptionsto overridebodyormethodif they're accidentally included. Spreading first ensures critical properties remain intact.♻️ Suggested defensive ordering
return fetch(this.url, { + ...this.fetchOptions, method: 'POST', - body: new FormData(this.$refs.form), - ...this.fetchOptions + body: new FormData(this.$refs.form) }).then((response) => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/javascript/submission_form/invite_form.vue` around lines 114 - 118, Spread this.fetchOptions before the explicit fetch properties so user-supplied options can't override critical fields: change the fetch call to pass ...this.fetchOptions first, then method: 'POST' and body: new FormData(this.$refs.form) (keeping this.url and the existing .then handler intact) so method and body set by the component (not this.fetchOptions) always win.app/javascript/submission_form/form.vue (1)
1475-1479: Apply the same defensive spread ordering here for consistency.Same concern as in
invite_form.vue— spreadingfetchOptionsafterbodyallows accidental override of the form data.♻️ Suggested defensive ordering
return fetch(this.baseUrl + this.submitPath, { + ...this.fetchOptions, method: 'POST', - body: formData || new FormData(this.$refs.form), - ...this.fetchOptions + body: formData || new FormData(this.$refs.form) }).then((response) => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/javascript/submission_form/form.vue` around lines 1475 - 1479, The fetch call in the submit handler spreads this.fetchOptions after setting body, which lets callers override the request body; update the spread order so explicit request fields come last by moving the spread of this.fetchOptions before body (i.e., call fetch(this.baseUrl + this.submitPath, { method: 'POST', ...this.fetchOptions, body: formData || new FormData(this.$refs.form) })) so body cannot be accidentally overwritten; update the code around the fetch invocation that constructs the request options (the fetch(...) call using this.baseUrl + this.submitPath, this.fetchOptions, body, formData, and this.$refs.form)..rubocop.yml (1)
86-87: Consider documenting the reason for increased block nesting.Increasing
Metrics/BlockNestingMax from 5 to 6 accommodates deeper nesting in the codebase. While this is a valid configuration choice, consider adding a comment explaining which files require this or refactoring the deeply nested code if feasible.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.rubocop.yml around lines 86 - 87, Add a brief comment above the Metrics/BlockNesting rule in .rubocop.yml explaining why Max was increased to 6 (e.g., list specific files or patterns that require deeper nesting or note an intentional tradeoff), or alternatively call out that those files should be reviewed for refactoring; reference the rule name "Metrics/BlockNesting" and the value "Max: 6" so reviewers can find and either accept the exception or target the named files for simplification.lib/replace_email_variables.rb (1)
172-174: Consider consistency withbuild_url_options_forfor custom domain support.
build_submission_linkstill usesDocuseal.default_url_optionsdirectly, whilebuild_submitter_linkandbuild_documents_links_textnow use the newbuild_url_options_forhelper. If custom domains should apply to submission links as well, consider using the helper here too.If this is intentional (e.g., submission links are for internal/admin use), this is fine as-is.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/replace_email_variables.rb` around lines 172 - 174, build_submission_link currently uses Docuseal.default_url_options directly while build_submitter_link and build_documents_links_text use the new build_url_options_for helper; if submission links should honor custom domains, change build_submission_link to call Rails.application.routes.url_helpers.submission_url(submission, **build_url_options_for(submission)) instead of using Docuseal.default_url_options so it uses the same domain resolution logic as build_submitter_link/build_documents_links_text; if submission links are intentionally internal, add a comment on build_submission_link explaining why default_url_options are used.app/jobs/send_test_webhook_request_job.rb (1)
39-49: Consider extracting signature computation to eliminate duplication.The signing logic (key retrieval, timestamp generation, HMAC computation, header names) duplicates
SendWebhookRequest.sign_request. If the signing algorithm changes, both locations need updating.Consider extracting the signature computation:
♻️ Proposed refactor to reuse signing logic
Add a method to
SendWebhookRequestthat computes the signature tuple:# In lib/send_webhook_request.rb def compute_signature(body, signing_key) timestamp = Time.current.to_i.to_s signature = OpenSSL::HMAC.hexdigest('SHA256', signing_key, "#{timestamp}.#{body}") { timestamp: timestamp, signature: "sha256=#{signature}" } endThen use it in both places:
- key = webhook_url.ensure_signing_key! - timestamp = Time.current.to_i.to_s - signature = OpenSSL::HMAC.hexdigest('SHA256', key, "#{timestamp}.#{body}") + sig = SendWebhookRequest.compute_signature(body, webhook_url.ensure_signing_key!) Faraday.post(webhook_url.url, body, 'Content-Type' => 'application/json', 'User-Agent' => USER_AGENT, - 'X-Webhook-Signature' => "sha256=#{signature}", - 'X-Webhook-Timestamp' => timestamp, + 'X-Webhook-Signature' => sig[:signature], + 'X-Webhook-Timestamp' => sig[:timestamp], 'X-Webhook-Request-Id' => SecureRandom.uuid, **webhook_url.secret.to_h)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/jobs/send_test_webhook_request_job.rb` around lines 39 - 49, The webhook signing logic in send_test_webhook_request_job.rb duplicates SendWebhookRequest.sign_request; extract the computation into a reusable method on SendWebhookRequest (e.g., add compute_signature(body, signing_key) that returns the timestamp and header value like { timestamp:, signature: "sha256=..." }) and change SendWebhookRequest.sign_request and SendTestWebhookRequestJob to call SendWebhookRequest.compute_signature (or delegate to a new public class method) so both places reuse the same HMAC/timestamp/header-name logic; update SendTestWebhookRequestJob to use webhook_url.ensure_signing_key! as the signing_key argument and set the X-Webhook-Timestamp and X-Webhook-Signature headers from the returned values.app/views/templates_preferences/_recipients.html.erb (1)
11-11: Extract submitter option resolution from the nested ternary.Line 11 is now too dense to safely evolve with new option types. Move this to a helper/presenter method to reduce branching risk.
♻️ Refactor direction
- <% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : (item.invite_via_field_uuid.present? ? 'invite_via_field' : ''))))) %> + <% item.option = submitter_option(item) %># e.g. helper def submitter_option(item) return 'is_requester' if item.is_requester.present? return 'email' if item.email.present? return "linked_to_#{item.linked_to_uuid}" if item.linked_to_uuid.present? return "invite_by_#{item.invite_by_uuid}" if item.invite_by_uuid.present? return "optional_invite_by_#{item.optional_invite_by_uuid}" if item.optional_invite_by_uuid.present? return 'invite_via_field' if item.invite_via_field_uuid.present? '' end🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/views/templates_preferences/_recipients.html.erb` at line 11, Extract the nested ternary logic from the template into a helper/presenter method (e.g., submitter_option(item)) that returns the correct option string for a given item by checking item.is_requester, item.email, item.linked_to_uuid, item.invite_by_uuid, item.optional_invite_by_uuid, and item.invite_via_field_uuid in that priority order; replace the inline assignment in the template with a call to submitter_option(item) so the view becomes simple and all branching lives in the helper/presenter.app/views/personalization_settings/_markdown_editor.html.erb (1)
69-69: Consider whetherrequired: trueis always appropriate for this field.The hidden field is unconditionally marked as
required, but the editor may be used in contexts where an empty body is acceptable. If empty content should be allowed in some cases, consider makingrequiredconfigurable vialocal_assigns.💡 Suggested approach
-<%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %> +<%= hidden_field_tag name, value, required: local_assigns.fetch(:required, true), data: { target: 'markdown-editor.textarea' } %>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/views/personalization_settings/_markdown_editor.html.erb` at line 69, The hidden_field_tag currently forces required: true unconditionally; make the requirement configurable via the partial's locals so contexts that allow empty content can pass required: false. Update the hidden_field_tag call (the one using hidden_field_tag with data: { target: 'markdown-editor.textarea' }) to use a local variable for the required flag (e.g. use local_assigns.fetch(:required, true) or a passed-in required local) instead of the hardcoded true and document the new required local for callers of the partial.lib/submitters/authorized_for_form.rb (1)
5-5: UnusedUnauthorizedexception class.The
Unauthorizedconstant is defined but never raised within this module. If it's intended for external use, consider adding a comment clarifying its purpose. Otherwise, it can be removed to avoid confusion.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/submitters/authorized_for_form.rb` at line 5, The Unauthorized constant (Unauthorized = Class.new(StandardError)) in the authorized_for_form module is defined but never used; either remove this unused exception to avoid confusion or, if it is intended for external consumers, add a one-line comment above the Unauthorized definition explaining its public purpose (e.g., "Raised by callers when authorization fails") and ensure any external API docs reference it; update the authorized_for_form file accordingly and run tests to confirm no references to Unauthorized remain.lib/submitters/submit_values.rb (2)
456-461: Inconsistent logging: Rollbar still used here while replaced elsewhere.This file shows mixed logging approaches. Line 458 uses
Rollbar.warningwhile other changes in this PR replace Rollbar withRails.logger. Consider updating for consistency.Proposed fix
def validate_value!(_value, field, _params, submitter, _request) if field['readonly'] == true - Rollbar.warning("Readonly field #{submitter.id}: #{field['uuid']}") if defined?(Rollbar) + Rails.logger.warn("Readonly field #{submitter.id}: #{field['uuid']}") raise ValidationError, 'Read-only field' end🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/submitters/submit_values.rb` around lines 456 - 461, The validate_value! method currently logs readonly-field warnings using Rollbar.warning; replace that call with Rails.logger.warn (or Rails.logger.warnf) to match the PR's logging convention: change the Rollbar.warning(...) in validate_value! to Rails.logger.warn with the same message including submitter.id and field['uuid'], and remove the defined?(Rollbar) guard; keep raising ValidationError as-is.
436-444: Loose phone validation may accept invalid values.
PHONE_REGEXP(/[+\d()\s-]+/) is very permissive and will match any string containing at least one digit, parenthesis, space, hyphen, or plus sign. Consider validating the phone number more strictly before creating the submitter, or at minimum checking for a reasonable minimum length after stripping non-digits.Proposed improvement
if value.include?('@') email = Submissions.normalize_email(value) elsif value.match?(PHONE_REGEXP) phone = value.gsub(/[^+\d]/, '') + phone = nil if phone.length < 7 # Minimum reasonable phone length end next if email.blank? && phone.blank?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/submitters/submit_values.rb` around lines 436 - 444, The phone validation using PHONE_REGEXP in submit_values.rb is too permissive and can let invalid numbers through before calling submission.submitters.create!; update the logic that sets phone (the branch that currently does phone = value.gsub(/[^+\d]/, '')) to strip non-digits into a variable (e.g., digits = value.gsub(/\D/, '')) and enforce a minimum reasonable length (e.g., digits.length >= 10) or use a stricter validator (Phonelib/libphonenumber) before assigning phone and proceeding to submission.submitters.create!; ensure you still handle international plus signs correctly and only call create! when the phone passes the new check.app/controllers/submit_form_controller.rb (1)
87-89: Early return incompletedaction may skip rendering.When
AuthorizedForForm.callpasses, the method returns without rendering anything explicitly. This relies on Rails' implicit template rendering (completed.html.erb). If the intention is to render the completed view, this works, but it might be clearer to use an explicitreturn renderor restructure as a guard clause withunless.Suggested clarification
def completed raise ActionController::RoutingError, I18n.t('not_found') if `@submitter.account.archived_at`? - return if Submitters::AuthorizedForForm.call(`@submitter`, current_user, request) - - redirect_to submit_form_path(params[:submit_form_slug]) + unless Submitters::AuthorizedForForm.call(`@submitter`, current_user, request) + return redirect_to submit_form_path(params[:submit_form_slug]) + end + + # Renders completed.html.erb end🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/controllers/submit_form_controller.rb` around lines 87 - 89, The early return in the completed action uses Submitters::AuthorizedForForm.call(`@submitter`, current_user, request) and returns without an explicit render, which relies on Rails implicit rendering; make it explicit by changing the control flow so that when the authorization check passes you call render :completed (or explicitly return render) and when it fails use the existing redirect_to submit_form_path(params[:submit_form_slug]) — alternatively rewrite as a guard clause using unless Submitters::AuthorizedForForm.call(...) then redirect, else render :completed to make the behavior clear.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.rubocop.yml:
- Around line 110-115: RuboCop flags a missing Rails environment "local" listed
under Rails/UnknownEnv; add a new config/environments/local.rb with the same
baseline settings used by development/production (e.g., class
ApplicationName::Application.configure block copying relevant development
config) so the "local" environment behaves as expected, or alternatively add a
short documented justification in the project README explaining why an explicit
local environment file is unnecessary and reference the files that rely on it
(lib/send_webhook_request.rb, lib/action_mailer_events_observer.rb,
app/controllers/templates_uploads_controller.rb) and the RuboCop entry to avoid
confusion.
In `@app/javascript/elements/markdown_editor.js`:
- Around line 156-164: The regex variable declared as const regex =
/\{\{?[a-zA-Z0-9_.-]+\}\}?/g retains state across calls to exec() causing missed
matches when iterating nodes in doc.descendants; update the loop in the
doc.descendants callback (the code that inspects node.text and uses regex.exec)
to reset regex.lastIndex = 0 before running exec on each node.text or create a
fresh RegExp instance per node (e.g., new RegExp(..., 'g')) so each text node is
scanned from the start and all variable highlights are found.
- Around line 378-380: disconnectedCallback currently calls
this.linkTooltip.hide() unconditionally which can throw if initialization was
skipped (e.g., missing textarea or editorElement), so update
disconnectedCallback to check that this.linkTooltip exists (and is not
null/undefined) before calling hide() (and similarly guard any other teardown
calls related to linkTooltip); locate the disconnectedCallback method and add a
simple existence guard around this.linkTooltip (and any other resources created
during initialization) to avoid runtime errors when the component was never
fully initialized.
- Around line 233-234: The current initialization sets content:
(this.textarea.value || '').trim().replace(/ *\n/g, '<br>') alongside
contentType: 'markdown', which corrupts markdown by converting newlines to <br>
and trimming user text; change the content assignment to pass the raw textarea
value (this.textarea.value) without .trim() or .replace() so the markdown parser
receives literal newlines and the editor preserves original content.
In `@app/javascript/elements/toggle_attribute.js`:
- Around line 6-10: The dataset.value parsing is asymmetric and returns the
string "true" instead of a boolean, breaking the comparison in the assignment
this.target[this.dataset.attribute] = value === dataValue; replace the current
expression that sets dataValue with a normalization that maps 'true' -> true,
'false' -> false, and when dataset.value is undefined default to true (or keep
non-boolean values as-is if intended), so comparisons (value === dataValue) work
consistently for checkboxes and other inputs referenced by
this.dataset.attribute.
In `@app/javascript/elements/toggle_classes.js`:
- Around line 3-15: Guard against missing DOM nodes and data attributes before
mutating classes: verify the `button` found by const button =
this.querySelector('a, button, label') exists and return early (or log) if not;
ensure `this.dataset.classes` is present and non-empty before calling .split('
'); when resolving `target` (const target = this.dataset.targetId ?
document.getElementById(this.dataset.targetId) : button) check that the element
returned is non-null and bail if it isn't (or fall back to `button`), and only
attach the click listener and call target.classList.add/remove/toggle when all
three (button, target, classes) are valid. Use clear warnings (console.warn or
similar) when preconditions fail.
In `@app/javascript/template_builder/formula_modal.vue`:
- Around line 169-171: The numeric-detection regex in isNumberField is too
permissive and should be replaced with a stricter pattern; update the check
inside isNumberField (and the options?.every callback) to use a regex that
allows an optional leading minus, digits, and at most one decimal point (for
example a pattern like one that matches "-123", "0.5", or ".5" but rejects
"1.2.3" or "..." ), and ensure you string-coerce the option value before testing
(e.g., String(o.value)). Keep the rest of the logic (checking field.type and
handling radio/select) unchanged.
In `@app/jobs/send_test_webhook_request_job.rb`:
- Line 29: The HTTPS-only validation in send_test_webhook_request_job.rb (the
raise HttpsError line) is stricter than production; update the check to match
the production behavior by honoring the same AccountConfig allow_http bypass
used in lib/send_webhook_request.rb (lines that reference
AccountConfig.allow_http) — either call the shared validation helper used for
regular webhooks or add the same conditional that permits HTTP when
account.config.allow_http is true, and keep raising HttpsError only when the
account does not allow HTTP and the URI is non-HTTPS; ensure you update the
validation logic in the SendTestWebhookRequestJob (or the method containing the
raise) so it references the account/config symbol used in production.
In `@app/models/account_config.rb`:
- Around line 95-97: The default SMS template defined by
SUBMITTER_INVITATION_SMS_KEY is hardcoded in English; change the lambda to call
I18n.t(...) with interpolation (e.g.,
I18n.t('account_config.submitter_invitation_sms', name: template.name, link:
submitter.link)) so it uses locale translations, and add the corresponding key
to your locale YAMLs (account_config.submitter_invitation_sms) with the same
interpolation placeholders; keep the lambda signature and interpolation keys
consistent with other defaults in account_config.rb.
In `@app/views/storage_settings/_aws_form.html.erb`:
- Line 11: The password field for :secret_access_key is marked required which
forces users to re-enter the secret when updating other settings; change the
options passed to fff.password_field so required is conditional (e.g., required:
configs['secret_access_key'].blank?) so that the field is only required when no
secret is already present, keeping the existing placeholder logic intact.
In `@app/views/storage_settings/_azure_form.html.erb`:
- Line 16: The password field for :storage_access_key is currently always
required which forces users to re-enter the key even when one is stored; update
the fff.password_field call for :storage_access_key to make the required
attribute conditional (required only when configs['storage_access_key'] is
blank) so existing stored keys remain editable without being forced to
re-submit, keep the masked placeholder behavior when
configs['storage_access_key'].present? and adjust only the required logic in the
fff.password_field invocation.
In `@app/views/storage_settings/_google_cloud_form.html.erb`:
- Line 16: The credentials textarea (f.field :credentials) is always marked
required which forces users to re-enter credentials on every save; change the
required option on the credentials textarea to be conditional so it is only
required when no saved credentials exist (e.g., use required:
configs['credentials'].blank? or remove the required key), keeping the
placeholder behavior and class/rows unchanged; locate the textarea using the
form builder variable fff and the :credentials field to update the options.
In `@app/views/submissions/_value.html.erb`:
- Around line 31-32: The view sets time_format = :detailed in
app/views/submissions/_value.html.erb when
local_assigns[:with_timestamp_seconds] is true, but the :detailed time format is
missing for several locales; fix this by adding time.formats.detailed entries to
config/locales/i18n.yml for the seven locales (Polish/pl, Ukrainian/uk,
Czech/cs, Hebrew/he, Arabic/ar, Korean/ko, Japanese/ja), following the style of
the existing :long format (i.e., a verbose format that includes seconds) so the
l(..., format: :detailed) call in the view renders correctly for those locales.
In `@app/views/submissions/show.html.erb`:
- Line 24: The link is using ActiveStorage::Blob.proxy_path which yields a
relative path and is inconsistent with other callers; change it to produce an
absolute URL by either swapping proxy_path to proxy_url on the blob call or,
preferably, call the model helper `@submission.audit_trail_url`(expires_at:
4.hours.from_now) (the Submission#audit_trail_url method) so the view uses the
same absolute URL generation used in webhooks and APIs.
In `@app/views/templates_preferences/_recipients.html.erb`:
- Line 119: Line 119's dead guard `<% if true %>` is showing the
ensure_unique_recipients form unconditionally; replace it with the proper
authorization check (e.g., mirror the pattern used for enforce_recipients_order
by using a permission or config check such as verifying
current_account.account_configs.exists?(...) or the same permission used
server-side like authorize!(:update, `@template`) via a view helper) or remove the
wrapper entirely and add a short comment stating the form is intentionally
visible and protected by controller-level authorize!(:update, `@template`); ensure
the change targets the ensure_unique_recipients block so visibility matches
other toggles.
In `@lib/markdown_to_html.rb`:
- Around line 12-13: The allowlist ALLOWED_TAGS currently omits HTML tags
emitted by the markdown parser, causing strikethrough and inline code to be
stripped; update the ALLOWED_TAGS constant to include "s" and "code" (and ensure
ALLOWED_ATTRIBUTES remains appropriate for those tags) so the methods that
render markdown (referenced by ALLOWED_TAGS and the markdown-to-HTML flow)
preserve strikethrough and code formatting after rendering.
In `@lib/submitters/authorized_for_form.rb`:
- Around line 36-39: The 2FA email normalization is inconsistent: replace the
inline normalization in the link 2FA key construction inside
authorized_for_form.rb (where link_2fa_key is built using
submitter.email.downcase.squish) with the canonical normalizer used for stored
emails by calling Submissions.normalize_email(submitter.email) so the key uses
the same normalized email; also update the other 2FA places
(submitter_mailer.rb, submit_form_email_2fas_controller.rb and lib/submitters.rb
where inline .downcase.strip or .downcase.squish are used) to use
Submissions.normalize_email(...) so both Email 2FA and Link 2FA compute
identical keys for verification via Submitter.signed_id_verifier.
In `@lib/submitters/submit_values.rb`:
- Line 446: The event currently logs the inviter's UUID by passing
submitter.uuid to SubmissionEvents.create_with_tracking_data for the
'invite_party' event; change the payload to include the newly created party's
UUID (s['uuid'] or the variable holding the invited party's uuid) instead of—or
in addition to—submitter.uuid so the tracking data refers to the invited party;
update the call to SubmissionEvents.create_with_tracking_data(submitter,
'invite_party', request, { uuid: s['uuid'] }) or { inviter_uuid: submitter.uuid,
invited_uuid: s['uuid'] } as appropriate.
In `@lib/templates/create_attachments.rb`:
- Around line 76-84: The current loop uses Zip::Entry#size (attacker-controlled)
to enforce MAX_ZIP_SIZE; instead, when iterating Zip::File.open(file.tempfile)
for each entry (skip directories), open the entry's input stream (e.g.,
entry.get_input_stream) and read it in chunks, accumulating actual bytes read
into total_size and immediately raise InvalidFileType, 'zip_too_large' if
total_size exceeds MAX_ZIP_SIZE; keep the rest of the extraction logic but
replace reliance on entry.size with this streamed-read guard to prevent zip-bomb
bypasses.
---
Outside diff comments:
In `@app/controllers/submit_form_values_controller.rb`:
- Around line 20-23: The render call is using an invalid head: :ok option;
update the controller to remove the head: :ok from the render json: call and
either (a) replace it with status: :ok on the render invocation (render json: {
value:, attachment: attachment&.as_json(...) }, status: :ok) or (b) remove the
option and call head :ok separately after any needed processing; target the
render json invocation that builds the response using value and
attachment&.as_json(...).
---
Duplicate comments:
In `@config/locales/i18n.yml`:
- Around line 650-651: There are duplicate YAML keys re_send_sms and send_sms;
remove the redundant declarations so each key is defined only once (keep the
original definitions and delete the later re_send_sms/send_sms entries), and
scan the locale file for any other repeated keys to prevent YAML key-collisions
(look for the symbols re_send_sms and send_sms to locate the duplicates).
- Around line 356-357: There is a duplicate locale key sms_has_been_sent (also
defined elsewhere) which can override or break YAML parsing; remove or
consolidate the duplicate by deleting the redundant sms_has_been_sent entry in
this block (or merge its value into the canonical sms_has_been_sent definition),
and keep only one authoritative sms_has_been_sent key; also verify
sms_has_been_sent_already is intentional and not a duplicate of another similar
key.
---
Nitpick comments:
In @.rubocop.yml:
- Around line 86-87: Add a brief comment above the Metrics/BlockNesting rule in
.rubocop.yml explaining why Max was increased to 6 (e.g., list specific files or
patterns that require deeper nesting or note an intentional tradeoff), or
alternatively call out that those files should be reviewed for refactoring;
reference the rule name "Metrics/BlockNesting" and the value "Max: 6" so
reviewers can find and either accept the exception or target the named files for
simplification.
In `@app/controllers/submit_form_controller.rb`:
- Around line 87-89: The early return in the completed action uses
Submitters::AuthorizedForForm.call(`@submitter`, current_user, request) and
returns without an explicit render, which relies on Rails implicit rendering;
make it explicit by changing the control flow so that when the authorization
check passes you call render :completed (or explicitly return render) and when
it fails use the existing redirect_to
submit_form_path(params[:submit_form_slug]) — alternatively rewrite as a guard
clause using unless Submitters::AuthorizedForForm.call(...) then redirect, else
render :completed to make the behavior clear.
In `@app/javascript/submission_form/form.vue`:
- Around line 1475-1479: The fetch call in the submit handler spreads
this.fetchOptions after setting body, which lets callers override the request
body; update the spread order so explicit request fields come last by moving the
spread of this.fetchOptions before body (i.e., call fetch(this.baseUrl +
this.submitPath, { method: 'POST', ...this.fetchOptions, body: formData || new
FormData(this.$refs.form) })) so body cannot be accidentally overwritten; update
the code around the fetch invocation that constructs the request options (the
fetch(...) call using this.baseUrl + this.submitPath, this.fetchOptions, body,
formData, and this.$refs.form).
In `@app/javascript/submission_form/invite_form.vue`:
- Around line 114-118: Spread this.fetchOptions before the explicit fetch
properties so user-supplied options can't override critical fields: change the
fetch call to pass ...this.fetchOptions first, then method: 'POST' and body: new
FormData(this.$refs.form) (keeping this.url and the existing .then handler
intact) so method and body set by the component (not this.fetchOptions) always
win.
In `@app/jobs/send_test_webhook_request_job.rb`:
- Around line 39-49: The webhook signing logic in
send_test_webhook_request_job.rb duplicates SendWebhookRequest.sign_request;
extract the computation into a reusable method on SendWebhookRequest (e.g., add
compute_signature(body, signing_key) that returns the timestamp and header value
like { timestamp:, signature: "sha256=..." }) and change
SendWebhookRequest.sign_request and SendTestWebhookRequestJob to call
SendWebhookRequest.compute_signature (or delegate to a new public class method)
so both places reuse the same HMAC/timestamp/header-name logic; update
SendTestWebhookRequestJob to use webhook_url.ensure_signing_key! as the
signing_key argument and set the X-Webhook-Timestamp and X-Webhook-Signature
headers from the returned values.
In `@app/views/personalization_settings/_markdown_editor.html.erb`:
- Line 69: The hidden_field_tag currently forces required: true unconditionally;
make the requirement configurable via the partial's locals so contexts that
allow empty content can pass required: false. Update the hidden_field_tag call
(the one using hidden_field_tag with data: { target: 'markdown-editor.textarea'
}) to use a local variable for the required flag (e.g. use
local_assigns.fetch(:required, true) or a passed-in required local) instead of
the hardcoded true and document the new required local for callers of the
partial.
In `@app/views/templates_preferences/_recipients.html.erb`:
- Line 11: Extract the nested ternary logic from the template into a
helper/presenter method (e.g., submitter_option(item)) that returns the correct
option string for a given item by checking item.is_requester, item.email,
item.linked_to_uuid, item.invite_by_uuid, item.optional_invite_by_uuid, and
item.invite_via_field_uuid in that priority order; replace the inline assignment
in the template with a call to submitter_option(item) so the view becomes simple
and all branching lives in the helper/presenter.
In `@lib/replace_email_variables.rb`:
- Around line 172-174: build_submission_link currently uses
Docuseal.default_url_options directly while build_submitter_link and
build_documents_links_text use the new build_url_options_for helper; if
submission links should honor custom domains, change build_submission_link to
call Rails.application.routes.url_helpers.submission_url(submission,
**build_url_options_for(submission)) instead of using
Docuseal.default_url_options so it uses the same domain resolution logic as
build_submitter_link/build_documents_links_text; if submission links are
intentionally internal, add a comment on build_submission_link explaining why
default_url_options are used.
In `@lib/submitters/authorized_for_form.rb`:
- Line 5: The Unauthorized constant (Unauthorized = Class.new(StandardError)) in
the authorized_for_form module is defined but never used; either remove this
unused exception to avoid confusion or, if it is intended for external
consumers, add a one-line comment above the Unauthorized definition explaining
its public purpose (e.g., "Raised by callers when authorization fails") and
ensure any external API docs reference it; update the authorized_for_form file
accordingly and run tests to confirm no references to Unauthorized remain.
In `@lib/submitters/submit_values.rb`:
- Around line 456-461: The validate_value! method currently logs readonly-field
warnings using Rollbar.warning; replace that call with Rails.logger.warn (or
Rails.logger.warnf) to match the PR's logging convention: change the
Rollbar.warning(...) in validate_value! to Rails.logger.warn with the same
message including submitter.id and field['uuid'], and remove the
defined?(Rollbar) guard; keep raising ValidationError as-is.
- Around line 436-444: The phone validation using PHONE_REGEXP in
submit_values.rb is too permissive and can let invalid numbers through before
calling submission.submitters.create!; update the logic that sets phone (the
branch that currently does phone = value.gsub(/[^+\d]/, '')) to strip non-digits
into a variable (e.g., digits = value.gsub(/\D/, '')) and enforce a minimum
reasonable length (e.g., digits.length >= 10) or use a stricter validator
(Phonelib/libphonenumber) before assigning phone and proceeding to
submission.submitters.create!; ensure you still handle international plus signs
correctly and only call create! when the phone passes the new check.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (10)
Gemfile.lockis excluded by!**/*.lockpublic/apple-icon-180x180.pngis excluded by!**/*.pngpublic/apple-touch-icon-precomposed.pngis excluded by!**/*.pngpublic/apple-touch-icon.pngis excluded by!**/*.pngpublic/favicon-16x16.pngis excluded by!**/*.pngpublic/favicon-32x32.pngis excluded by!**/*.pngpublic/favicon-96x96.pngis excluded by!**/*.pngpublic/favicon.icois excluded by!**/*.icopublic/favicon.svgis excluded by!**/*.svgyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (111)
.rubocop.ymlDockerfileGemfileapp/controllers/api/submissions_controller.rbapp/controllers/api/submitters_controller.rbapp/controllers/api/templates_controller.rbapp/controllers/submissions_download_controller.rbapp/controllers/submit_form_controller.rbapp/controllers/submit_form_decline_controller.rbapp/controllers/submit_form_download_controller.rbapp/controllers/submit_form_draw_signature_controller.rbapp/controllers/submit_form_invite_controller.rbapp/controllers/submit_form_values_controller.rbapp/controllers/template_documents_controller.rbapp/controllers/templates_controller.rbapp/controllers/templates_recipients_controller.rbapp/controllers/templates_uploads_controller.rbapp/javascript/application.jsapp/javascript/application.scssapp/javascript/elements/markdown_editor.jsapp/javascript/elements/toggle_attribute.jsapp/javascript/elements/toggle_classes.jsapp/javascript/submission_form/completed.vueapp/javascript/submission_form/form.vueapp/javascript/submission_form/i18n.jsapp/javascript/submission_form/initials_step.vueapp/javascript/submission_form/invite_form.vueapp/javascript/submission_form/signature_step.vueapp/javascript/submission_form/validate_signature.jsapp/javascript/template_builder/builder.vueapp/javascript/template_builder/formula_modal.vueapp/javascript/template_builder/i18n.jsapp/javascript/template_builder/page.vueapp/jobs/send_test_webhook_request_job.rbapp/mailers/submitter_mailer.rbapp/models/account_config.rbapp/views/email_smtp_settings/index.html.erbapp/views/icons/_arrow_back_up.html.erbapp/views/icons/_arrow_forward_up.html.erbapp/views/icons/_bold.html.erbapp/views/icons/_italic.html.erbapp/views/icons/_underline.html.erbapp/views/layouts/application.html.erbapp/views/personalization_settings/_email_body_field.html.erbapp/views/personalization_settings/_markdown_editor.html.erbapp/views/personalization_settings/_submitter_completed_email_form.html.erbapp/views/pwa/manifest.json.erbapp/views/shared/_meta.html.erbapp/views/shared/_search_input.html.erbapp/views/start_form/_policy.html.erbapp/views/storage_settings/_aws_form.html.erbapp/views/storage_settings/_azure_form.html.erbapp/views/storage_settings/_google_cloud_form.html.erbapp/views/submissions/_detailed_form.html.erbapp/views/submissions/_email_form.html.erbapp/views/submissions/_phone_form.html.erbapp/views/submissions/_send_email_base.html.erbapp/views/submissions/_value.html.erbapp/views/submissions/show.html.erbapp/views/submissions_dashboard/index.html.erbapp/views/submissions_filters/_applied_filters.html.erbapp/views/submissions_filters/_filter_modal.html.erbapp/views/submit_form/show.html.erbapp/views/submitter_mailer/_custom_content.html.erbapp/views/submitter_mailer/documents_copy_email.html.erbapp/views/submitter_mailer/invitation_email.html.erbapp/views/templates/_submission.html.erbapp/views/templates/show.html.erbapp/views/templates_preferences/_recipients.html.erbapp/views/templates_preferences/_submitter_completed_email_form.html.erbapp/views/templates_preferences/_submitter_documents_copy_email_form.html.erbapp/views/templates_preferences/_submitter_invitation_email_form.html.erbapp/views/templates_prefillable_fields/_form.html.erbapp/views/templates_share_link/show.html.erbapp/views/webhook_settings/show.html.erbconfig/dotenv.rbconfig/initializers/active_storage.rbconfig/locales/i18n.ymlconfig/routes.rblib/download_utils.rblib/load_active_storage_configs.rblib/markdown_to_html.rblib/replace_email_variables.rblib/send_webhook_request.rblib/submissions.rblib/submissions/create_from_submitters.rblib/submissions/generate_audit_trail.rblib/submissions/generate_preview_attachments.rblib/submissions/generate_result_attachments.rblib/submitters.rblib/submitters/authorized_for_form.rblib/submitters/form_configs.rblib/submitters/normalize_values.rblib/submitters/submit_values.rblib/template_folders.rblib/templates.rblib/templates/clone.rblib/templates/create_attachments.rbpackage.jsonspec/jobs/send_form_completed_webhook_request_job_spec.rbspec/jobs/send_form_declined_webhook_request_job_spec.rbspec/jobs/send_form_started_webhook_request_job_spec.rbspec/jobs/send_form_viewed_webhook_request_job_spec.rbspec/jobs/send_submission_completed_webhook_request_job_spec.rbspec/jobs/send_submission_created_webhook_request_job_spec.rbspec/jobs/send_submission_expired_webhook_request_job_spec.rbspec/jobs/send_template_created_webhook_request_job_spec.rbspec/jobs/send_template_updated_webhook_request_job_spec.rbspec/rails_helper.rbspec/system/email_settings_spec.rbspec/system/signing_form_spec.rb
💤 Files with no reviewable changes (1)
- spec/system/email_settings_spec.rb
✅ Files skipped from review due to trivial changes (1)
- app/views/icons/_italic.html.erb
🚧 Files skipped from review as they are similar to previous changes (5)
- app/javascript/submission_form/completed.vue
- app/controllers/api/submitters_controller.rb
- app/views/email_smtp_settings/index.html.erb
- app/views/templates_preferences/_submitter_documents_copy_email_form.html.erb
- app/javascript/submission_form/signature_step.vue
| Rails/UnknownEnv: | ||
| Environments: | ||
| - development | ||
| - test | ||
| - production | ||
| - local |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify that a local environment configuration exists
echo "=== Looking for local.rb in config/environments ==="
fd -t f 'local.rb' config/environments 2>/dev/null || echo "No local.rb found"
echo ""
echo "=== Checking for references to Rails.env.local or RAILS_ENV=local ==="
rg -n 'Rails\.env\.local|RAILS_ENV.*local' --type ruby 2>/dev/null || echo "No matches found"
echo ""
echo "=== Checking for any environment configuration references ==="
rg -n 'config/environments' --type ruby | head -20
echo ""
echo "=== Looking at the config/environments directory structure ==="
ls -la config/environments/ 2>/dev/null || echo "config/environments directory not found"Repository: kencove/docuseal
Length of output: 949
Add config/environments/local.rb or document the local environment configuration.
The local environment is actively used in the codebase (found in 5 files including lib/send_webhook_request.rb, lib/action_mailer_events_observer.rb, and app/controllers/templates_uploads_controller.rb), but there is no corresponding config/environments/local.rb file. Create this file with appropriate Rails environment configuration for the local environment, or document why explicit configuration is not needed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.rubocop.yml around lines 110 - 115, RuboCop flags a missing Rails
environment "local" listed under Rails/UnknownEnv; add a new
config/environments/local.rb with the same baseline settings used by
development/production (e.g., class ApplicationName::Application.configure block
copying relevant development config) so the "local" environment behaves as
expected, or alternatively add a short documented justification in the project
README explaining why an explicit local environment file is unnecessary and
reference the files that rely on it (lib/send_webhook_request.rb,
lib/action_mailer_events_observer.rb,
app/controllers/templates_uploads_controller.rb) and the RuboCop entry to avoid
confusion.
| const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g | ||
|
|
||
| doc.descendants((node, pos) => { | ||
| if (!node.isText) return | ||
|
|
||
| let match | ||
|
|
||
| while ((match = regex.exec(node.text)) !== null) { | ||
| decorations.push( |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "markdown_editor.js" -type fRepository: kencove/docuseal
Length of output: 104
🏁 Script executed:
cat -n app/javascript/elements/markdown_editor.js | sed -n '150,170p'Repository: kencove/docuseal
Length of output: 911
🏁 Script executed:
cat -n app/javascript/elements/markdown_editor.js | head -200 | tail -100Repository: kencove/docuseal
Length of output: 3427
Reset regex state per text node to avoid missed variable highlights.
Lines 156-164 reuse a global regex with exec(); lastIndex carries between different node.text values and skips earlier matches in subsequent nodes.
🐛 Proposed fix
doc.descendants((node, pos) => {
if (!node.isText) return
+ regex.lastIndex = 0
let match
while ((match = regex.exec(node.text)) !== null) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/javascript/elements/markdown_editor.js` around lines 156 - 164, The regex
variable declared as const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g retains state
across calls to exec() causing missed matches when iterating nodes in
doc.descendants; update the loop in the doc.descendants callback (the code that
inspects node.text and uses regex.exec) to reset regex.lastIndex = 0 before
running exec on each node.text or create a fresh RegExp instance per node (e.g.,
new RegExp(..., 'g')) so each text node is scanned from the start and all
variable highlights are found.
| content: (this.textarea.value || '').trim().replace(/ *\n/g, '<br>'), | ||
| contentType: 'markdown', |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, find and examine the markdown_editor.js file
find . -name "markdown_editor.js" -type fRepository: kencove/docuseal
Length of output: 104
🏁 Script executed:
#!/bin/bash
# Read the file around lines 233-234 to understand context
cat -n app/javascript/elements/markdown_editor.js | sed -n '220,250p'Repository: kencove/docuseal
Length of output: 1204
🏁 Script executed:
#!/bin/bash
# Get the full file structure to understand what this object is
wc -l app/javascript/elements/markdown_editor.jsRepository: kencove/docuseal
Length of output: 106
🏁 Script executed:
#!/bin/bash
# Get more context - the class structure and initialization
cat -n app/javascript/elements/markdown_editor.js | sed -n '1,50p'Repository: kencove/docuseal
Length of output: 2718
🏁 Script executed:
#!/bin/bash
# Get context before the problematic lines to understand the object structure
cat -n app/javascript/elements/markdown_editor.js | sed -n '200,245p'Repository: kencove/docuseal
Length of output: 1624
🏁 Script executed:
#!/bin/bash
# Look at the class that contains this code
cat -n app/javascript/elements/markdown_editor.js | sed -n '100,200p'Repository: kencove/docuseal
Length of output: 3465
🏁 Script executed:
#!/bin/bash
# Search for any comments or related code about the content transformation
rg -B 10 -A 10 "contentType.*markdown" app/javascript/elements/markdown_editor.jsRepository: kencove/docuseal
Length of output: 751
🏁 Script executed:
#!/bin/bash
# Search for tests related to markdown editor initialization
find . -name "*.test.js" -o -name "*.spec.js" | xargs grep -l "markdown_editor" 2>/dev/null | head -5Repository: kencove/docuseal
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check if there are any other references to this transformation pattern
rg "trim.*replace.*\\\\n" --type jsRepository: kencove/docuseal
Length of output: 178
🏁 Script executed:
#!/bin/bash
# Search for any documentation or comments explaining the <br> conversion
rg -B 5 "replace.*\\\\\*\\\n.*<br>" app/javascript/Repository: kencove/docuseal
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check git history to see if there was a reason for this transformation
git log --oneline -p -- app/javascript/elements/markdown_editor.js | grep -B 10 -A 10 "replace.*<br>" | head -50Repository: kencove/docuseal
Length of output: 727
🏁 Script executed:
#!/bin/bash
# Look at the TipTap Markdown extension documentation reference
rg "Markdown" app/javascript/elements/markdown_editor.js -B 5 -A 5Repository: kencove/docuseal
Length of output: 1424
Remove incompatible content transformations that corrupt markdown during editor initialization.
When contentType: 'markdown' is set, TipTap expects raw markdown syntax with literal newlines. Passing <br> HTML tags to the markdown parser treats them as literal text, breaking the markdown format. Additionally, .trim() unnecessarily mutates the original content before any user edits. Pass the raw textarea value without transformations:
Proposed fix
- content: (this.textarea.value || '').trim().replace(/ *\n/g, '<br>'),
+ content: this.textarea.value || '',
contentType: 'markdown',📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| content: (this.textarea.value || '').trim().replace(/ *\n/g, '<br>'), | |
| contentType: 'markdown', | |
| content: this.textarea.value || '', | |
| contentType: 'markdown', |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/javascript/elements/markdown_editor.js` around lines 233 - 234, The
current initialization sets content: (this.textarea.value ||
'').trim().replace(/ *\n/g, '<br>') alongside contentType: 'markdown', which
corrupts markdown by converting newlines to <br> and trimming user text; change
the content assignment to pass the raw textarea value (this.textarea.value)
without .trim() or .replace() so the markdown parser receives literal newlines
and the editor preserves original content.
| disconnectedCallback () { | ||
| this.linkTooltip.hide() | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n app/javascript/elements/markdown_editor.js | head -200Repository: kencove/docuseal
Length of output: 7777
🏁 Script executed:
cat -n app/javascript/elements/markdown_editor.js | sed -n '145,220p'Repository: kencove/docuseal
Length of output: 2597
🏁 Script executed:
cat -n app/javascript/elements/markdown_editor.js | sed -n '220,320p'Repository: kencove/docuseal
Length of output: 3429
🏁 Script executed:
cat -n app/javascript/elements/markdown_editor.js | sed -n '360,390p'Repository: kencove/docuseal
Length of output: 878
Guard teardown when initialization was skipped.
Line 379 calls this.linkTooltip.hide() unconditionally. If the early return at line 147 is triggered (missing textarea or editorElement), the linkTooltip property is never initialized, causing a runtime error when disconnectedCallback is called.
🛠️ Proposed fix
disconnectedCallback () {
- this.linkTooltip.hide()
+ this.linkTooltip?.hide()
if (this.editor) {
this.editor.destroy()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| disconnectedCallback () { | |
| this.linkTooltip.hide() | |
| disconnectedCallback () { | |
| this.linkTooltip?.hide() | |
| if (this.editor) { | |
| this.editor.destroy() |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/javascript/elements/markdown_editor.js` around lines 378 - 380,
disconnectedCallback currently calls this.linkTooltip.hide() unconditionally
which can throw if initialization was skipped (e.g., missing textarea or
editorElement), so update disconnectedCallback to check that this.linkTooltip
exists (and is not null/undefined) before calling hide() (and similarly guard
any other teardown calls related to linkTooltip); locate the
disconnectedCallback method and add a simple existence guard around
this.linkTooltip (and any other resources created during initialization) to
avoid runtime errors when the component was never fully initialized.
| const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value | ||
| const dataValue = this.dataset.value === 'false' ? false : this.dataset.value || true | ||
|
|
||
| if (this.dataset.attribute) { | ||
| this.target[this.dataset.attribute] = event.target.checked | ||
| this.target[this.dataset.attribute] = value === dataValue |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n app/javascript/elements/toggle_attribute.jsRepository: kencove/docuseal
Length of output: 1324
Normalize data-value booleans symmetrically ("true" and "false").
The current code at line 7 has asymmetric parsing: this.dataset.value === 'false' ? false : this.dataset.value || true. When data-value="false", the condition matches and returns boolean false. When data-value="true", the condition fails and evaluates "true" || true, which returns the string "true" (non-empty strings are truthy). This means checkbox comparisons on lines 10, 14, and 17 will fail (true !== "true"), inverting the intended behavior for data-value="true" cases.
The fix correctly normalizes both string representations to explicit boolean values, ensuring consistent comparisons regardless of which data attribute value is used.
Proposed fix
- const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
- const dataValue = this.dataset.value === 'false' ? false : this.dataset.value || true
+ const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
+ const rawDataValue = this.dataset.value
+ const dataValue =
+ rawDataValue === undefined
+ ? true
+ : rawDataValue === 'true'
+ ? true
+ : rawDataValue === 'false'
+ ? false
+ : rawDataValue📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value | |
| const dataValue = this.dataset.value === 'false' ? false : this.dataset.value || true | |
| if (this.dataset.attribute) { | |
| this.target[this.dataset.attribute] = event.target.checked | |
| this.target[this.dataset.attribute] = value === dataValue | |
| const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value | |
| const rawDataValue = this.dataset.value | |
| const dataValue = | |
| rawDataValue === undefined | |
| ? true | |
| : rawDataValue === 'true' | |
| ? true | |
| : rawDataValue === 'false' | |
| ? false | |
| : rawDataValue | |
| if (this.dataset.attribute) { | |
| this.target[this.dataset.attribute] = value === dataValue |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/javascript/elements/toggle_attribute.js` around lines 6 - 10, The
dataset.value parsing is asymmetric and returns the string "true" instead of a
boolean, breaking the comparison in the assignment
this.target[this.dataset.attribute] = value === dataValue; replace the current
expression that sets dataValue with a normalization that maps 'true' -> true,
'false' -> false, and when dataset.value is undefined default to true (or keep
non-boolean values as-is if intended), so comparisons (value === dataValue) work
consistently for checkboxes and other inputs referenced by
this.dataset.attribute.
| <% end %> | ||
| <% end %> | ||
| <% if can?(:manage, :personalization_advanced) %> | ||
| <% if true %> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Check the actual template file and context around line 119 =="
if fd 'templates_preferences/_recipients.html.erb' app; then
FILE=$(fd 'templates_preferences/_recipients.html.erb' app)
echo "Found file: $FILE"
echo
echo "=== Lines 110-130 of the template ==="
sed -n '110,130p' "$FILE" | cat -n
fi
echo
echo "== Locate template preferences controller =="
if fd 'templates_preferences_controller\.rb$' app; then
CTRL=$(fd 'templates_preferences_controller\.rb$' app)
echo "Found controller: $CTRL"
echo
echo "=== Controller update action ==="
sed -n '1,200p' "$CTRL" | cat -n
fi
echo
echo "== Check for policy files ==="
fd '*policy*.rb' app | head -20
echo
echo "== Search for template_preferences authorization/update patterns ==="
rg -n 'def update|authorize|policy_scope|TemplatesPreferences' app/controllers app/policies 2>/dev/null || echo "No matches found"Repository: kencove/docuseal
Length of output: 237
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search for _recipients.html.erb template ==="
find app -name "_recipients.html.erb" -o -name "*recipients*" -type f | head -20
echo
echo "== List templates_preferences directory ==="
find app/views -type d -name "*templates_preferences*" 2>/dev/null || echo "Directory not found"
echo
echo "== Search for any templates_preferences files ==="
find app -path "*templates_preferences*" 2>/dev/null || echo "No matches"
echo
echo "== Broader search in views directory ==="
ls -la app/views/ | grep -i template || echo "No template dirs found"Repository: kencove/docuseal
Length of output: 2028
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Read the _recipients.html.erb file around line 119 =="
wc -l app/views/templates_preferences/_recipients.html.erb
echo
echo "=== Full template (showing with line numbers) ==="
cat -n app/views/templates_preferences/_recipients.html.erb
echo
echo "== Check the controller ==="
echo "=== templates_preferences_controller.rb ==="
cat -n app/controllers/templates_preferences_controller.rbRepository: kencove/docuseal
Length of output: 17467
Remove or replace the dead if true guard with proper authorization check.
Line 119's <% if true %> is a dead guard that unconditionally shows the "ensure_unique_recipients" form to all users. While server-side authorization (the authorize!(:update, @template) in the controller's create action on line 18) prevents unauthorized updates, the form visibility should be gated consistently with the pattern used elsewhere in this file (e.g., the "enforce_recipients_order" toggle at line 105 uses unless current_account.account_configs.exists?(...)).
Either:
- Restore the intended authorization/permission condition, or
- Remove the wrapper entirely with a comment explaining this preference is intentionally visible to all with server-side protection.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/views/templates_preferences/_recipients.html.erb` at line 119, Line 119's
dead guard `<% if true %>` is showing the ensure_unique_recipients form
unconditionally; replace it with the proper authorization check (e.g., mirror
the pattern used for enforce_recipients_order by using a permission or config
check such as verifying current_account.account_configs.exists?(...) or the same
permission used server-side like authorize!(:update, `@template`) via a view
helper) or remove the wrapper entirely and add a short comment stating the form
is intentionally visible and protected by controller-level authorize!(:update,
`@template`); ensure the change targets the ensure_unique_recipients block so
visibility matches other toggles.
| ALLOWED_TAGS = %w[p br strong b em i u a].freeze | ||
| ALLOWED_ATTRIBUTES = %w[href].freeze |
There was a problem hiding this comment.
Allowlist is dropping generated markdown tags (<s> and <code>).
Line 12 currently omits tags that are produced by the parser (Line 7 and Line 106), so strikethrough/code formatting gets removed after rendering.
💡 Proposed fix
- ALLOWED_TAGS = %w[p br strong b em i u a].freeze
+ ALLOWED_TAGS = %w[p br strong b em i u s code a].freeze📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ALLOWED_TAGS = %w[p br strong b em i u a].freeze | |
| ALLOWED_ATTRIBUTES = %w[href].freeze | |
| ALLOWED_TAGS = %w[p br strong b em i u s code a].freeze | |
| ALLOWED_ATTRIBUTES = %w[href].freeze |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/markdown_to_html.rb` around lines 12 - 13, The allowlist ALLOWED_TAGS
currently omits HTML tags emitted by the markdown parser, causing strikethrough
and inline code to be stripped; update the ALLOWED_TAGS constant to include "s"
and "code" (and ensure ALLOWED_ATTRIBUTES remains appropriate for those tags) so
the methods that render markdown (referenced by ALLOWED_TAGS and the
markdown-to-HTML flow) preserve strikethrough and code formatting after
rendering.
| if (token = request.params[:two_factor_token].presence || request.headers['x-two-factor-token'].presence) | ||
| link_2fa_key = [submitter.email.downcase.squish, submitter.submission.template.slug].join(':') | ||
|
|
||
| return true if Submitter.signed_id_verifier.verified(token, purpose: :email_two_factor) == link_2fa_key |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for email normalization patterns
rg -n "normalize.*email|email.*normalize" --type ruby -A 2Repository: kencove/docuseal
Length of output: 3191
🏁 Script executed:
#!/bin/bash
# Check how downcase is used with emails
rg -n "\.downcase\b" --type ruby -B 2 -A 1 | grep -i email -B 2 -A 1Repository: kencove/docuseal
Length of output: 4372
🏁 Script executed:
#!/bin/bash
# Look for the specific file mentioned: lib/submissions.rb
fd -t f "submissions.rb" -exec grep -n "normalize_email" {} +Repository: kencove/docuseal
Length of output: 290
🏁 Script executed:
#!/bin/bash
# Search for 2FA-related email handling
rg -n "two_factor|2fa|2FA" --type ruby -B 2 -A 2Repository: kencove/docuseal
Length of output: 24684
🏁 Script executed:
#!/bin/bash
# Find where EmailVerificationCodes.generate is called with link_2fa related context
rg -n "EmailVerificationCodes\.generate" --type ruby -B 3 -A 1Repository: kencove/docuseal
Length of output: 761
🏁 Script executed:
#!/bin/bash
# Check the full normalize_email implementation to understand its scope
sed -n '168,198p' lib/submissions.rbRepository: kencove/docuseal
Length of output: 1079
🏁 Script executed:
#!/bin/bash
# Search for where signed_id_verifier is used to sign tokens for 2FA
rg -n "signed_id.*verifier.*sign" --type ruby -B 2 -A 2Repository: kencove/docuseal
Length of output: 1480
Standardize email normalization across 2FA verification flows.
The codebase has two 2FA implementations using different email normalization methods:
- Email 2FA (submitter_mailer.rb, submit_form_email_2fas_controller.rb): uses
.downcase.strip - Link 2FA (lib/submitters.rb:259, authorized_for_form.rb:37): uses
.downcase.squish
While submitter.email is already normalized via Submissions.normalize_email when stored in the database (handling gmail typos, domain corrections, etc.), the inconsistent normalization methods during 2FA key construction create maintenance risk. Consider using Submissions.normalize_email consistently or standardizing on a single normalization method across both 2FA flows.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/submitters/authorized_for_form.rb` around lines 36 - 39, The 2FA email
normalization is inconsistent: replace the inline normalization in the link 2FA
key construction inside authorized_for_form.rb (where link_2fa_key is built
using submitter.email.downcase.squish) with the canonical normalizer used for
stored emails by calling Submissions.normalize_email(submitter.email) so the key
uses the same normalized email; also update the other 2FA places
(submitter_mailer.rb, submit_form_email_2fas_controller.rb and lib/submitters.rb
where inline .downcase.strip or .downcase.squish are used) to use
Submissions.normalize_email(...) so both Email 2FA and Link 2FA compute
identical keys for verification via Submitter.signed_id_verifier.
|
|
||
| submission.submitters.create!(uuid: s['uuid'], email:, phone:, account_id: submitter.account_id) | ||
|
|
||
| SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid }) |
There was a problem hiding this comment.
Event tracking may log incorrect submitter UUID.
The invite_party event is created for the original submitter who filled the form, but the uuid in the data refers to submitter.uuid (the inviter), not the newly created party's UUID. Consider whether the data should include the invited party's UUID (s['uuid']) for better traceability.
Proposed fix
- SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
+ SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { invited_uuid: s['uuid'] })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/submitters/submit_values.rb` at line 446, The event currently logs the
inviter's UUID by passing submitter.uuid to
SubmissionEvents.create_with_tracking_data for the 'invite_party' event; change
the payload to include the newly created party's UUID (s['uuid'] or the variable
holding the invited party's uuid) instead of—or in addition to—submitter.uuid so
the tracking data refers to the invited party; update the call to
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, {
uuid: s['uuid'] }) or { inviter_uuid: submitter.uuid, invited_uuid: s['uuid'] }
as appropriate.
| total_size = 0 | ||
|
|
||
| Zip::File.open(file.tempfile).each do |entry| | ||
| next if entry.directory? | ||
|
|
||
| total_size += entry.size | ||
|
|
||
| raise InvalidFileType, 'zip_too_large' if total_size > MAX_ZIP_SIZE | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Finding rubyzip version from lockfiles ==="
fd 'Gemfile.lock' -x sh -c 'echo "--- {}"; rg -A2 -B2 "^\s+rubyzip" "{}" || echo "rubyzip not found in {}"'
echo
echo "=== Examining the actual code at lib/templates/create_attachments.rb ==="
cat -n lib/templates/create_attachments.rb | sed -n '70,95p'
echo
echo "=== Locating ZIP extraction callsites and size checks ==="
rg -n 'Zip::File\.open|entry\.size|IO\.copy_stream|zip_too_large' --type ruby -C3Repository: kencove/docuseal
Length of output: 2695
🌐 Web query:
rubyzip Zip::Entry#size behavior untrusted archives security
💡 Result:
- In rubyzip,
Zip::Entry#sizeis just the entry’s declared uncompressed size read from the ZIP headers (@size), i.e. metadata (def size;@SiZe|| 0; end). It is not computed by decompressing the entry. [1] - For untrusted archives, that value must be treated as attacker-controlled: rubyzip explicitly documents/acknowledges that the “uncompressed size reported in the zip can be spoofed,” which historically enabled bypassing app “max size” checks and led to zip-bomb style disk-exhaustion DoS (CVE-2019-16892; fixed in rubyzip ≥ 1.3.0). [2][3]
- Mitigations recommended by rubyzip:
- Check
entry.sizebefore extraction (as a policy limit), but don’t rely on it alone unless you also use rubyzip’s extraction-time validation. [4][2] - Use rubyzip versions where extraction size validation exists; rubyzip’s README describes
Zip.validate_entry_sizes(and notes thatZip::InputStreamdoes no size checking—callers must enforce limits while reading). [4] - Still enforce global limits (max files, total uncompressed bytes, and/or streaming read caps), because
entry.sizeis only per-entry metadata. [4][2]
- Check
Sources:
[1] rubyzip Zip::Entry#size implementation (source) (raw.githubusercontent.com)
[2] rubyzip PR #403 discussion (“uncompressed size … can be spoofed”, Zip.validate_entry_sizes) (github.com)
[3] NVD CVE-2019-16892 description (nvd.nist.gov)
[4] rubyzip README (“Size Validation”, Zip.validate_entry_sizes, InputStream note) (github.com)
Citations:
- 1: https://raw.githubusercontent.com/rubyzip/rubyzip/main/lib/zip/entry.rb
- 2: Validate entry sizes when extracting rubyzip/rubyzip#403
- 3: https://nvd.nist.gov/vuln/detail/CVE-2019-16892?utm_source=openai
- 4: https://github.com/rubyzip/rubyzip
Enforce ZIP extraction size limits at stream level, not only on ZIP metadata.
Line 81 relies on Zip::Entry#size, which is attacker-controlled metadata from the ZIP header. Per CVE-2019-16892 and rubyzip's own documentation, this value can be spoofed to bypass size checks, enabling zip-bomb DoS attacks. The guard at line 83 is insufficient—enforce the size limit while actually reading bytes during extraction.
Proposed fix
- total_size = 0
+ total_size = 0
@@
- total_size += entry.size
-
- raise InvalidFileType, 'zip_too_large' if total_size > MAX_ZIP_SIZE
-
tempfile = Tempfile.new(entry.name)
tempfile.binmode
- entry.get_input_stream { |in_stream| IO.copy_stream(in_stream, tempfile) }
+ entry.get_input_stream do |in_stream|
+ while (chunk = in_stream.read(64.kilobytes))
+ total_size += chunk.bytesize
+ raise InvalidFileType, 'zip_too_large' if total_size > MAX_ZIP_SIZE
+ tempfile.write(chunk)
+ end
+ end
tempfile.rewind🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/templates/create_attachments.rb` around lines 76 - 84, The current loop
uses Zip::Entry#size (attacker-controlled) to enforce MAX_ZIP_SIZE; instead,
when iterating Zip::File.open(file.tempfile) for each entry (skip directories),
open the entry's input stream (e.g., entry.get_input_stream) and read it in
chunks, accumulating actual bytes read into total_size and immediately raise
InvalidFileType, 'zip_too_large' if total_size exceeds MAX_ZIP_SIZE; keep the
rest of the extraction logic but replace reliance on entry.size with this
streamed-read guard to prevent zip-bomb bypasses.
Replace remaining hardcoded "DocuSeal" in application_mailer.rb From header and pages/landing.html.erb title with Docuseal.product_name. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (2)
app/javascript/template_builder/formula_modal.vue (1)
169-171:⚠️ Potential issue | 🟡 MinorNumeric-option regex is still too permissive.
Line 170 still accepts invalid numeric strings, so some non-numeric option sets can be treated as numeric fields.
🔧 Suggested fix
isNumberField (field) { - return field.type === 'number' || (['radio', 'select'].includes(field.type) && field.options?.every((o) => String(o.value).match(/^[\d.-]+$/))) + return field.type === 'number' || ( + ['radio', 'select'].includes(field.type) && + field.options?.every((o) => /^-?(?:\d+|\d*\.\d+)$/.test(String(o.value))) + ) },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/javascript/template_builder/formula_modal.vue` around lines 169 - 171, The isNumberField check uses a too-permissive regex; replace the current /^[\d.-]+$/ test in the options.every predicate with a stricter, anchored numeric check (for example trim the string and test against /^-?\d+(\.\d+)?$/ or similar) so values like "--" or "1.2.3" are rejected; update the options check inside isNumberField (the function name isNumberField and its options?.every callback) to use String(o.value).trim().match(/^-?\d+(\.\d+)?$/) or alternatively coerce and validate with Number.isFinite(Number(trimmedValue)).lib/submissions/generate_result_attachments.rb (1)
284-284:⚠️ Potential issue | 🟠 MajorException logging still loses stack traces in these rescue blocks.
This is still using
Rails.logger.error(e), which logs message text without full backtrace context in these failure paths.#!/bin/bash # Verify all remaining exception logs that pass the exception object directly. rg -nP 'Rails\.logger\.error\(e\)' lib/submissions/generate_result_attachments.rbAlso applies to: 744-745, 759-760, 844-845, 866-867
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/submissions/generate_result_attachments.rb` at line 284, Replace the bare Rails.logger.error(e) calls in the rescue blocks of lib/submissions/generate_result_attachments.rb with a log that includes the exception class, message and full backtrace (e.g. use e.full_message or build "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}") so the stack trace is preserved; update every occurrence of Rails.logger.error(e) in that file (the rescue handlers around the result-attachment generation) to use the full-message/backtrace logging form.
🧹 Nitpick comments (3)
spec/jobs/send_submitter_invitation_sms_job_spec.rb (1)
47-77: Missing test forsubmitter.template.archived_atskip condition.The job returns early when
submitter.template&.archived_at?(line 16 in job file), but there's no test covering this branch. Consider adding a test:it 'skips when template is archived' do submitter.template.update!(archived_at: Time.current) described_class.new.perform('submitter_id' => submitter.id) expect(WebMock).not_to have_requested(:post, twilio_url) end🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@spec/jobs/send_submitter_invitation_sms_job_spec.rb` around lines 47 - 77, Add a spec that covers the job early-return when a submitter's template is archived: in send_submitter_invitation_sms_job_spec.rb create an example that updates submitter.template.archived_at to Time.current, calls described_class.new.perform('submitter_id' => submitter.id), and asserts WebMock did not post to twilio_url; this mirrors the existing "skips when ..." tests and targets the submitter.template&.archived_at? condition checked in the job.spec/lib/send_sms_spec.rb (2)
67-74: Test does not verify what its name claims.The test is named "sets open_timeout and read_timeout on the request" but only asserts that a POST was made. WebMock cannot verify Faraday's request options. Consider either removing this test (the implementation sets timeouts, which is already code-reviewed) or renaming it to reflect what it actually verifies.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@spec/lib/send_sms_spec.rb` around lines 67 - 74, The spec "sets open_timeout and read_timeout on the request" is misleading because it only asserts a POST was made (expect(WebMock).to have_requested...) and does not verify Faraday timeouts; update the test in spec/lib/send_sms_spec.rb to either remove it or rename it to reflect the actual assertion (for example rename to "makes a POST request to Twilio" and keep the WebMock expectation against twilio_url after calling described_class.call(**call_params)), or alternatively implement a proper timeout verification by stubbing/inspecting the Faraday connection used by described_class.call if you want to assert timeouts.
60-65: Test description doesn't match the expected exception.The test is named "propagates Faraday::TimeoutError" but expects
Faraday::ConnectionFailed. WebMock'sto_timeoutraisesFaraday::ConnectionFailed, so the expectation is correct—consider updating the test name to match.✏️ Suggested rename
- it 'propagates Faraday::TimeoutError on network timeout' do + it 'propagates Faraday::ConnectionFailed on network timeout' do stub_request(:post, twilio_url).to_timeout🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@spec/lib/send_sms_spec.rb` around lines 60 - 65, Update the example description in the spec so it matches the actual exception being raised: in spec/lib/send_sms_spec.rb, change the it block description that currently says "propagates Faraday::TimeoutError on network timeout" to accurately state "propagates Faraday::ConnectionFailed on network timeout" (the test exercises described_class.call(**call_params) with WebMock.to_timeout which results in Faraday::ConnectionFailed).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/rate_limit.rb`:
- Around line 13-14: Currently the rescue StandardError that unconditionally
returns ActiveSupport::Cache::MemoryStore silently degrades Redis-backed rate
limiting; change this to only fallback when Redis is truly not configured or
unreachable and surface/log the actual error instead of swallowing it: catch
only specific exceptions (e.g., Redis::BaseConnectionError or
Errno::ECONNREFUSED) around the Redis client/connector code in
lib/rate_limit.rb, call Rails.logger.error (or the existing logger) with the
error and context, and either re-raise for non-transient/unexpected errors or
fallback to MemoryStore only when a configured fallback policy allows it; ensure
the rescue no longer uses a blanket rescue StandardError and references the
existing Redis client/connection initializer in rate_limit.rb.
In `@lib/submissions/generate_result_attachments.rb`:
- Line 40: SIGN_REASON embeds Docuseal.product_name into a format string and
will break if the product name contains '%' characters; fix by escaping percent
signs when constructing SIGN_REASON (e.g. use
Docuseal.product_name.gsub('%','%%') when interpolating) so format(SIGN_REASON,
name: ...) won't raise; also apply the same percent-escaping to any other places
that interpolate Docuseal.product_name into format strings that feed into the
format() calls referencing SIGN_REASON.
- Line 327: The assignment of time_format in generate_result_attachments.rb uses
:detailed when with_timestamp_seconds is true but many locales lack
time.formats.detailed and I18n.l will raise; update the logic around the
time_format variable (the block that sets time_format based on
with_timestamp_seconds) to check I18n.exists?('time.formats.detailed',
I18n.locale) (or equivalent) and only use :detailed when that format key exists
for the current locale, otherwise fall back to :long; apply this change where
time_format is set and any subsequent calls to I18n.l that use time_format so
attachments won't crash on missing translations.
In `@spec/lib/rate_limit_spec.rb`:
- Around line 45-49: The example currently short-circuits when ENV['REDIS_URL']
is present so it makes no assertion; update the spec to force a blank REDIS_URL
for the example and then assert described_class::STORE is an
ActiveSupport::Cache::MemoryStore. Concretely, within the example ensure
ENV['REDIS_URL'] is stubbed or temporarily set to nil/blank (e.g., use RSpec
stubbing or a helper like ClimateControl) before referencing
described_class::STORE, then perform the explicit expect(store).to
be_a(ActiveSupport::Cache::MemoryStore); ensure the ENV change is only scoped to
the example so other tests are unaffected.
- Around line 51-62: The test currently constructs a local store variable and
asserts it is an ActiveSupport::Cache::Store, which doesn't verify the module's
fallback; change the test to inspect described_class::STORE (not the local
store) and assert it is an ActiveSupport::Cache::MemoryStore
(expect(described_class::STORE).to be_a(ActiveSupport::Cache::MemoryStore)); if
necessary arrange the invalid Redis URL or trigger the module initialization
path so described_class::STORE is set before the expectation.
---
Duplicate comments:
In `@app/javascript/template_builder/formula_modal.vue`:
- Around line 169-171: The isNumberField check uses a too-permissive regex;
replace the current /^[\d.-]+$/ test in the options.every predicate with a
stricter, anchored numeric check (for example trim the string and test against
/^-?\d+(\.\d+)?$/ or similar) so values like "--" or "1.2.3" are rejected;
update the options check inside isNumberField (the function name isNumberField
and its options?.every callback) to use
String(o.value).trim().match(/^-?\d+(\.\d+)?$/) or alternatively coerce and
validate with Number.isFinite(Number(trimmedValue)).
In `@lib/submissions/generate_result_attachments.rb`:
- Line 284: Replace the bare Rails.logger.error(e) calls in the rescue blocks of
lib/submissions/generate_result_attachments.rb with a log that includes the
exception class, message and full backtrace (e.g. use e.full_message or build
"#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}") so the stack trace is
preserved; update every occurrence of Rails.logger.error(e) in that file (the
rescue handlers around the result-attachment generation) to use the
full-message/backtrace logging form.
---
Nitpick comments:
In `@spec/jobs/send_submitter_invitation_sms_job_spec.rb`:
- Around line 47-77: Add a spec that covers the job early-return when a
submitter's template is archived: in send_submitter_invitation_sms_job_spec.rb
create an example that updates submitter.template.archived_at to Time.current,
calls described_class.new.perform('submitter_id' => submitter.id), and asserts
WebMock did not post to twilio_url; this mirrors the existing "skips when ..."
tests and targets the submitter.template&.archived_at? condition checked in the
job.
In `@spec/lib/send_sms_spec.rb`:
- Around line 67-74: The spec "sets open_timeout and read_timeout on the
request" is misleading because it only asserts a POST was made
(expect(WebMock).to have_requested...) and does not verify Faraday timeouts;
update the test in spec/lib/send_sms_spec.rb to either remove it or rename it to
reflect the actual assertion (for example rename to "makes a POST request to
Twilio" and keep the WebMock expectation against twilio_url after calling
described_class.call(**call_params)), or alternatively implement a proper
timeout verification by stubbing/inspecting the Faraday connection used by
described_class.call if you want to assert timeouts.
- Around line 60-65: Update the example description in the spec so it matches
the actual exception being raised: in spec/lib/send_sms_spec.rb, change the it
block description that currently says "propagates Faraday::TimeoutError on
network timeout" to accurately state "propagates Faraday::ConnectionFailed on
network timeout" (the test exercises described_class.call(**call_params) with
WebMock.to_timeout which results in Faraday::ConnectionFailed).
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (24)
app/javascript/template_builder/builder.vueapp/javascript/template_builder/conditions_modal.vueapp/javascript/template_builder/formula_modal.vueapp/jobs/send_submitter_invitation_sms_job.rbapp/mailers/application_mailer.rbapp/views/layouts/_head_tags.html.erbapp/views/pages/landing.html.erbapp/views/shared/_email_attribution.html.erbapp/views/shared/_meta.html.erbapp/views/shared/_powered_by.html.erbapp/views/start_form/_docuseal_logo.html.erbapp/views/start_form/email_verification.html.erbapp/views/start_form/private.html.erbapp/views/start_form/show.html.erbapp/views/submit_form/email_2fa.html.erbapp/views/submit_form/show.html.erblib/docuseal.rblib/rate_limit.rblib/send_sms.rblib/submissions/generate_audit_trail.rblib/submissions/generate_result_attachments.rbspec/jobs/send_submitter_invitation_sms_job_spec.rbspec/lib/rate_limit_spec.rbspec/lib/send_sms_spec.rb
💤 Files with no reviewable changes (1)
- app/javascript/template_builder/conditions_modal.vue
🚧 Files skipped from review as they are similar to previous changes (4)
- lib/submissions/generate_audit_trail.rb
- app/views/submit_form/show.html.erb
- app/javascript/template_builder/builder.vue
- app/views/shared/_meta.html.erb
| rescue StandardError | ||
| ActiveSupport::Cache::MemoryStore.new |
There was a problem hiding this comment.
Avoid silent fallback when Redis is configured.
At Line 13-14, rescuing all errors and silently switching to MemoryStore can weaken global rate-limiting guarantees (per-process counters) without any signal.
Suggested hardening
- rescue StandardError
- ActiveSupport::Cache::MemoryStore.new
+ rescue StandardError => e
+ Rails.logger.error("RateLimit store init failed: #{e.class}: #{e.message}")
+ ActiveSupport::Cache::MemoryStore.new
end📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| rescue StandardError | |
| ActiveSupport::Cache::MemoryStore.new | |
| rescue StandardError => e | |
| Rails.logger.error("RateLimit store init failed: #{e.class}: #{e.message}") | |
| ActiveSupport::Cache::MemoryStore.new | |
| end |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/rate_limit.rb` around lines 13 - 14, Currently the rescue StandardError
that unconditionally returns ActiveSupport::Cache::MemoryStore silently degrades
Redis-backed rate limiting; change this to only fallback when Redis is truly not
configured or unreachable and surface/log the actual error instead of swallowing
it: catch only specific exceptions (e.g., Redis::BaseConnectionError or
Errno::ECONNREFUSED) around the Redis client/connector code in
lib/rate_limit.rb, call Rails.logger.error (or the existing logger) with the
error and context, and either re-raise for non-transient/unexpected errors or
fallback to MemoryStore only when a configured fallback policy allows it; ensure
the rescue no longer uses a blanket rescue StandardError and references the
existing Redis client/connection initializer in rate_limit.rb.
| }.freeze | ||
|
|
||
| SIGN_REASON = 'Signed by %<name>s with DocuSeal.com' | ||
| SIGN_REASON = "Signed by %<name>s with #{Docuseal.product_name}" |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Ruby Kernel#format unescaped percent sign ArgumentError
💡 Result:
In Ruby, Kernel#format/sprintf treat every % in the format string as the start of a format directive (%[flags][width][.precision]type). If Ruby can’t parse a valid directive, it raises ArgumentError like “malformed format string - %X”. (docs.ruby-lang.org)
Fix: escape a literal percent
Use %% to output a literal %:
format("You get 10%% OFF!") #=> "You get 10% OFF!"
format("Hi %s, you get 10%% OFF!", "Jo") #=> "Hi Jo, you get 10% OFF!"
``` ([stackoverflow.com](https://stackoverflow.com/questions/13432122/string-interpolation-with-actual-in-string?utm_source=openai))
### Fix: when using named placeholders, include a type (or use `%{name}`)
If you write `%<name>` without a trailing type, Ruby will treat the next character as the type (e.g. `'`) and error. Use `%<name>s` or `%{name}`:
```rb
format("class='btn %<additional>s'", additional: "x") # OK
format("class='btn %{additional}'", additional: "x") # OK (equivalent to %<additional>s)
``` ([stackoverflow.com](https://stackoverflow.com/questions/52086595/ruby-format-string-with-variable-inside-quotes?utm_source=openai))
Citations:
- 1: https://docs.ruby-lang.org/en/master/language/format_specifications_rdoc.html?utm_source=openai
- 2: https://stackoverflow.com/questions/13432122/string-interpolation-with-actual-in-string?utm_source=openai
- 3: https://stackoverflow.com/questions/52086595/ruby-format-string-with-variable-inside-quotes?utm_source=openai
---
🏁 Script executed:
```shell
# First, let's locate and examine the file
find . -name "generate_result_attachments.rb" -type fRepository: kencove/docuseal
Length of output: 108
🏁 Script executed:
# Examine the file around line 40 and the other mentioned lines (922-924, 929-930)
cat -n lib/submissions/generate_result_attachments.rb | sed -n '35,45p; 920,935p'Repository: kencove/docuseal
Length of output: 932
SIGN_REASON breaks when product name contains % characters.
Line 40 uses string interpolation to embed Docuseal.product_name into the format string. If the configured product name contains % (e.g., 100%), it becomes an unescaped % in the constant, causing format() to raise ArgumentError when called at lines 923 and 929. This breaks PDF signing flows.
🐛 Proposed fix
- SIGN_REASON = "Signed by %<name>s with #{Docuseal.product_name}"
+ SIGN_REASON = 'Signed by %<name>s with %<product_name>s'
@@
- def sign_reason(name)
- format(SIGN_REASON, name:)
+ def sign_reason(name)
+ format(SIGN_REASON, name:, product_name: Docuseal.product_name)
end
@@
- format(SIGN_REASON, name: signers.reverse.join(', '))
+ format(SIGN_REASON, name: signers.reverse.join(', '), product_name: Docuseal.product_name)Also applies to: 922-924, 929-930
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/submissions/generate_result_attachments.rb` at line 40, SIGN_REASON
embeds Docuseal.product_name into a format string and will break if the product
name contains '%' characters; fix by escaping percent signs when constructing
SIGN_REASON (e.g. use Docuseal.product_name.gsub('%','%%') when interpolating)
so format(SIGN_REASON, name: ...) won't raise; also apply the same
percent-escaping to any other places that interpolate Docuseal.product_name into
format strings that feed into the format() calls referencing SIGN_REASON.
| timezone = submitter.account.timezone | ||
| timezone = submitter.timezone || submitter.account.timezone if with_submitter_timezone | ||
|
|
||
| time_format = with_timestamp_seconds ? :detailed : :long |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, let's see the context around line 327 in the file
wc -l lib/submissions/generate_result_attachments.rbRepository: kencove/docuseal
Length of output: 110
🏁 Script executed:
#!/bin/bash
# Read the relevant section around line 327
sed -n '310,345p' lib/submissions/generate_result_attachments.rb | cat -nRepository: kencove/docuseal
Length of output: 1919
🏁 Script executed:
#!/bin/bash
# Check how time_format is used after assignment
sed -n '327,360p' lib/submissions/generate_result_attachments.rb | cat -nRepository: kencove/docuseal
Length of output: 1950
🏁 Script executed:
#!/bin/bash
# Search for :detailed format in locale files
find . -path ./node_modules -prune -o -type f \( -name '*.yml' -o -name '*.yaml' \) -print | xargs rg -l ':detailed' | head -20Repository: kencove/docuseal
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check for time.formats.detailed in locale files
rg -A2 'time:' config/locales/*.yml | grep -A2 'formats:'Repository: kencove/docuseal
Length of output: 587
🏁 Script executed:
#!/bin/bash
# List all locale files in config/locales
find config/locales -name '*.yml' -o -name '*.yaml' | sortRepository: kencove/docuseal
Length of output: 83
🏁 Script executed:
#!/bin/bash
# Check which locale files have time.formats.detailed
for file in $(find config/locales -name '*.yml' -o -name '*.yaml' | sort); do
if grep -q "detailed:" "$file"; then
echo "✓ $(basename $file)"
else
echo "✗ $(basename $file)"
fi
doneRepository: kencove/docuseal
Length of output: 70
🏁 Script executed:
#!/bin/bash
# Check the actual structure of locale files - look for time.formats section
find config/locales -name '*.yml' -o -name '*.yaml' | xargs rg -B2 -A5 'formats:'Repository: kencove/docuseal
Length of output: 2110
🏁 Script executed:
#!/bin/bash
# Search for other uses of I18n.l with format: in the codebase to see patterns
rg "I18n\.l\(" -A2 | grep -A2 "format:" | head -40Repository: kencove/docuseal
Length of output: 2103
🏁 Script executed:
#!/bin/bash
# Get the full i18n.yml file to see complete locale coverage
cat config/locales/i18n.yml | head -200Repository: kencove/docuseal
Length of output: 10170
🏁 Script executed:
#!/bin/bash
# Search for sections that might NOT have detailed format
# Check Polish section specifically
rg -A30 "^pl:" config/locales/i18n.ymlRepository: kencove/docuseal
Length of output: 1883
🏁 Script executed:
#!/bin/bash
# Check Arabic section
rg -A30 "^ar:" config/locales/i18n.ymlRepository: kencove/docuseal
Length of output: 1580
🏁 Script executed:
#!/bin/bash
# Verify I18n.exists? API and usage in Rails/I18n context
rg "I18n\.exists\?" lib/ app/ --no-heading -B1 -A1Repository: kencove/docuseal
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check if there are any error handling or rescue blocks related to I18n format errors
rg -B3 -A3 "rescue.*I18n\|MissingTranslationData"Repository: kencove/docuseal
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check the full content of i18n.yml to see all locale sections
tail -300 config/locales/i18n.ymlRepository: kencove/docuseal
Length of output: 14508
🏁 Script executed:
#!/bin/bash
# Count how many locales are supported and which have detailed format
rg "^[a-z]{2}(-[A-Z]{2})?:" config/locales/i18n.yml | sed 's/:.*//g' | sort -uRepository: kencove/docuseal
Length of output: 149
🏁 Script executed:
#!/bin/bash
# Now check which of those locales actually have the detailed format defined
for locale in $(rg "^[a-z]{2}(-[A-Z]{2})?:" config/locales/i18n.yml | sed 's/:.*//g' | sort -u); do
if rg "^$locale:" -A 500 config/locales/i18n.yml | grep -q "detailed:"; then
echo "✓ $locale has :detailed"
else
echo "✗ $locale MISSING :detailed"
fi
doneRepository: kencove/docuseal
Length of output: 569
🏁 Script executed:
#!/bin/bash
# Check Rails I18n behavior for missing format keys
# Search for examples of how Rails/I18n handles missing keys in this codebase
rg "rescue.*I18n\|I18n.*rescue" --no-heading -B2 -A2Repository: kencove/docuseal
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check if there are any other guards or error handling for format keys
rg "time\.formats" lib/ app/ --no-headingRepository: kencove/docuseal
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Verify what the actual default locale is
rg "config\.i18n\.default_locale\|I18n\.default_locale" --no-headingRepository: kencove/docuseal
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check if I18n.exists? is used anywhere and how
rg "I18n\.exists\?" --no-heading -B1 -A1Repository: kencove/docuseal
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check Rails/Ruby version to understand I18n behavior
head -50 Gemfile | grep -E "rails|bundler"Repository: kencove/docuseal
Length of output: 165
🏁 Script executed:
#!/bin/bash
# Check if there's a config file or initializer for I18n
find . -path ./node_modules -prune -o -name "*i18n*" -type f -print | grep -E "config|initializer"Repository: kencove/docuseal
Length of output: 85
🏁 Script executed:
#!/bin/bash
# Look at actual line 332-336 to see what "Also applies to" refers to
sed -n '327,340p' lib/submissions/generate_result_attachments.rb | cat -nRepository: kencove/docuseal
Length of output: 1013
🏁 Script executed:
#!/bin/bash
# Check Rails gem version and I18n behavior notes
cat Gemfile.lock | grep -A2 "rails-i18n\|rails ("Repository: kencove/docuseal
Length of output: 725
Guard :detailed datetime format or it will crash on locales without this format definition.
When with_timestamp_seconds is enabled, 77% of supported locales (cs, de, de-DE, en, es, es-ES, fr, fr-FR, he, it, it-IT, nl, nl-NL, pl, pt, pt-PT, uk) are missing the time.formats.detailed key. Calling I18n.l(..., format: :detailed) will raise I18n::MissingTranslationData and fail attachment generation.
💡 Suggested fix
- time_format = with_timestamp_seconds ? :detailed : :long
+ time_format =
+ if with_timestamp_seconds && I18n.exists?('time.formats.detailed', locale)
+ :detailed
+ else
+ :long
+ end🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/submissions/generate_result_attachments.rb` at line 327, The assignment
of time_format in generate_result_attachments.rb uses :detailed when
with_timestamp_seconds is true but many locales lack time.formats.detailed and
I18n.l will raise; update the logic around the time_format variable (the block
that sets time_format based on with_timestamp_seconds) to check
I18n.exists?('time.formats.detailed', I18n.locale) (or equivalent) and only use
:detailed when that format key exists for the current locale, otherwise fall
back to :long; apply this change where time_format is set and any subsequent
calls to I18n.l that use time_format so attachments won't crash on missing
translations.
| it 'uses MemoryStore when REDIS_URL is not set' do | ||
| store = described_class::STORE | ||
|
|
||
| expect(store).to be_a(ActiveSupport::Cache::MemoryStore) if ENV['REDIS_URL'].blank? | ||
| end |
There was a problem hiding this comment.
This spec can pass without validating its claim.
At Line 48, the inline condition means the example does no assertion when REDIS_URL is present, so it may pass without checking behavior.
Make the expectation explicit
- expect(store).to be_a(ActiveSupport::Cache::MemoryStore) if ENV['REDIS_URL'].blank?
+ expect(ENV['REDIS_URL']).to be_blank
+ expect(store).to be_a(ActiveSupport::Cache::MemoryStore)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@spec/lib/rate_limit_spec.rb` around lines 45 - 49, The example currently
short-circuits when ENV['REDIS_URL'] is present so it makes no assertion; update
the spec to force a blank REDIS_URL for the example and then assert
described_class::STORE is an ActiveSupport::Cache::MemoryStore. Concretely,
within the example ensure ENV['REDIS_URL'] is stubbed or temporarily set to
nil/blank (e.g., use RSpec stubbing or a helper like ClimateControl) before
referencing described_class::STORE, then perform the explicit expect(store).to
be_a(ActiveSupport::Cache::MemoryStore); ensure the ENV change is only scoped to
the example so other tests are unaffected.
| it 'falls back to MemoryStore on Redis connection error' do | ||
| store = begin | ||
| redis_url = 'redis://invalid-host:6379/0' | ||
| ActiveSupport::Cache::RedisCacheStore.new(url: redis_url, namespace: 'rate_limit') | ||
| rescue StandardError | ||
| ActiveSupport::Cache::MemoryStore.new | ||
| end | ||
|
|
||
| # The RedisCacheStore may be created without raising (it connects lazily), | ||
| # but our module's rescue block ensures a MemoryStore fallback on any error | ||
| expect(store).to be_a(ActiveSupport::Cache::Store) | ||
| end |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n -C5 "falls back to MemoryStore on Redis connection error" spec/lib/rate_limit_spec.rbRepository: kencove/docuseal
Length of output: 527
🏁 Script executed:
cat spec/lib/rate_limit_spec.rbRepository: kencove/docuseal
Length of output: 2297
Test does not verify MemoryStore fallback and tests wrong object.
The test creates a local store variable rather than testing described_class::STORE, and the assertion at line 61 (be_a(ActiveSupport::Cache::Store)) accepts both RedisCacheStore and MemoryStore, so it will pass even if the fallback never executes. The inline comment acknowledges that RedisCacheStore connects lazily and that the module has fallback logic, but the test doesn't actually validate either. Change the assertion to be_a(ActiveSupport::Cache::MemoryStore) at minimum, or better yet, test the actual described_class::STORE configuration to confirm the module's fallback behavior works.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@spec/lib/rate_limit_spec.rb` around lines 51 - 62, The test currently
constructs a local store variable and asserts it is an
ActiveSupport::Cache::Store, which doesn't verify the module's fallback; change
the test to inspect described_class::STORE (not the local store) and assert it
is an ActiveSupport::Cache::MemoryStore (expect(described_class::STORE).to
be_a(ActiveSupport::Cache::MemoryStore)); if necessary arrange the invalid Redis
URL or trigger the module initialization path so described_class::STORE is set
before the expectation.
Ruby 4.0 / Rails 8.1 no longer bundles redis by default. RedisCacheStore requires it at load time, causing a LoadError (not StandardError) that crashes the app before the rescue in rate_limit.rb can catch it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary by CodeRabbit
New Features
Improvements
Infrastructure