Skip to content

refactor(lookupRecord): harden JXA escaping using shared helpers#35

Open
ebowman wants to merge 2 commits into
dvcrn:mainfrom
ebowman:fix/lookuprecord-escaping
Open

refactor(lookupRecord): harden JXA escaping using shared helpers#35
ebowman wants to merge 2 commits into
dvcrn:mainfrom
ebowman:fix/lookuprecord-escaping

Conversation

@ebowman

@ebowman ebowman commented May 25, 2026

Copy link
Copy Markdown
Contributor

Stacked on #34. This branch contains both commits; review the second commit (refactor(lookupRecord): harden JXA escaping…) for the changes specific to this PR. Once #34 lands and is rebased, this PR's diff will shrink to just the escaping changes.

Problem

The lookup_record script template interpolates user-controlled strings raw:

case "filename":
  searchResults = theApp.lookupRecordsWithFile("${value}", { in: searchDatabase });

A value containing a double quote, backslash, or newline breaks the JXA at runtime. There's also no boundary check for control characters (NUL, etc.) which can't be safely escaped at all. Same shape of issue as #26 (lost apostrophes in create_record) and #32 (executeJxa shell-escape), but at the script-template layer.

Fix

Mirror the pattern already used in src/tools/search.ts and src/tools/importFile.ts:

  1. Pre-validate string inputs (value, databaseName, every entry in tags) with isJXASafeString. If any contains a control character (0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F, 0x7F), return a structured error before any JXA is built.
  2. Pre-escape every interpolated value using formatValueForJXA / escapeStringForJXA, then declare them as const p* at the top of the script (pValue, pDatabaseName, pLookupType, pTags, pMatchAnyTag, pLimit). The script body references these constants and never re-interpolates raw input.

The functional behavior from #34 — cross-database scope, x-devonthink-item:// UUID shortcut, matchAnyTag handling, UUID dedup — is preserved unchanged.

Test plan

  • npm test — 34/34 pass (7 new escaping tests in tests/tools/lookupRecord.test.ts):
    • Double quote in value is escaped (evil"injection.md"evil\"injection.md")
    • Backslash and newline in value are escaped
    • Double quote in databaseName is escaped
    • Double quote inside tags[] entries is escaped
    • Control character in value is rejected at the boundary
    • Control character in databaseName is rejected at the boundary
    • Control character inside tags[] is rejected at the boundary
  • npm run build clean
  • npm run format:check clean
  • End-to-end verified against a live DEVONthink session: normal lookups still work, quote-bearing and newline-bearing values no longer crash the JXA, NUL-byte inputs are rejected with the expected error.

Notes for the maintainer

  • The 7 new tests use \x00, \x01, \x07 (TypeScript hex escapes) to keep the source as text — git would otherwise treat the file as binary.
  • I considered also tightening escapeStringForJXA to escape the few additional Unicode line-terminators (, ), but those don't appear in DEVONthink content in practice and would be a separate change touching multiple tools. Happy to file a follow-up if you'd like.

Eric Bowman added 2 commits May 25, 2026 15:04
…itted

Previously, when `databaseName` was not supplied, `lookup_record` silently
scoped to `theApp.currentDatabase()`. This caused lookups to miss records
that exist in other open databases, with no signal to the caller that the
scope had been narrowed — `success: true` with zero results.

DEVONthink's `lookupRecordsWith*` AppleScript APIs (unlike `search`)
don't natively span all open databases when `in:` is omitted, so the fix
iterates `theApp.databases()` and unions the per-database results,
deduplicating by UUID. The `x-devonthink-item://` URL shortcut still
resolves globally via `getRecordWithUuid` and short-circuits the loop.

Schema and tool descriptions updated to document the new default.

Tests: 11 unit tests verifying script generation across all six
`lookupType` branches, scoping behavior with and without `databaseName`,
URL shortcut, matchAnyTag, and UUID dedup.
The previous lookupRecord script template interpolated user-controlled
strings (value, databaseName, lookupType, tags) raw — e.g.
`"${value}"`. Any value containing a double quote, backslash, or newline
would break the JXA at runtime, and there was no boundary check for
control characters that can't be safely escaped at all. This mirrors the
issue addressed for create/update by PR dvcrn#32 (executeJxa shell-escape)
and issue dvcrn#26 (lost apostrophes), but at the script-template layer.

Changes:
- Pre-validate string inputs with `isJXASafeString`; reject control
  characters (0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F, 0x7F) with a structured
  error before any JXA is built.
- Build all interpolated values as safely-escaped JXA literals up front
  using `formatValueForJXA` / `escapeStringForJXA` and declare them as
  `const p*` at the top of the script. The body references `pValue`,
  `pDatabaseName`, `pLookupType`, `pTags`, `pMatchAnyTag`, `pLimit`
  rather than re-interpolating raw input. Mirrors the pattern in
  `src/tools/search.ts` and `src/tools/importFile.ts`.
- The `x-devonthink-item://` URL shortcut, the `matchAnyTag` flag, the
  cross-database UUID dedup, and the default-scope semantics from PR1
  are preserved unchanged.

Tests: 7 new unit tests covering quote/backslash/newline escaping in
value, databaseName, and tags; plus control-character rejection on each
of those inputs.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request enhances the lookup_record tool by implementing input validation for JXA safety, improving string escaping, and expanding the default search scope to include all open databases. It also adds result deduplication and a specialized shortcut for x-devonthink-item:// URLs, supported by a new suite of unit tests. Feedback focuses on adhering to the project's style guide regarding JXA interpreter limitations, specifically requiring the use of bracket notation for object property assignments and returns within the script templates to avoid potential runtime errors.

Comment thread src/tools/lookupRecord.ts
Comment on lines 106 to 110
return JSON.stringify({
success: false,
error: "Database not found: ${databaseName}"
error: "Database not found: " + pDatabaseName
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The JXA interpreter has known limitations with object literal syntax when generated via template literals. According to the repository style guide, objects should be built using bracket notation before being stringified or returned to avoid potential ReferenceError issues.

Suggested change
return JSON.stringify({
success: false,
error: "Database not found: ${databaseName}"
error: "Database not found: " + pDatabaseName
});
}
const errorRes = {};
errorRes["success"] = false;
errorRes["error"] = "Database not found: " + pDatabaseName;
return JSON.stringify(errorRes);
References
  1. JXA cannot return object literals directly. Always build objects using bracket notation before returning. (link)

Comment thread src/tools/lookupRecord.ts

switch (pLookupType) {
case "filename":
dbResults = theApp.lookupRecordsWithFile(pValue, { in: searchDatabase });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Avoid using inline object literals as arguments in JXA scripts. It is safer to define an options object using bracket notation first.

Suggested change
dbResults = theApp.lookupRecordsWithFile(pValue, { in: searchDatabase });
const options = {};
options["in"] = searchDatabase;
dbResults = theApp.lookupRecordsWithFile(pValue, options);
References
  1. Use bracket notation for object property assignment to avoid JXA interpreter limitations. (link)

Comment thread src/tools/lookupRecord.ts
Comment on lines +165 to +168
const tagOptions = { in: searchDatabase };
if (pMatchAnyTag) {
tagOptions.any = true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Refactor tagOptions to use bracket notation for property assignment to ensure compatibility with the JXA interpreter.

Suggested change
const tagOptions = { in: searchDatabase };
if (pMatchAnyTag) {
tagOptions.any = true;
}
const tagOptions = {};
tagOptions["in"] = searchDatabase;
if (pMatchAnyTag) {
tagOptions["any"] = true;
}
References
  1. Use bracket notation for object property assignment. (link)

Comment thread src/tools/lookupRecord.ts

// Include URL if available
if (record.url && record.url()) {
result.url = record.url();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Use bracket notation for property assignment.

Suggested change
result.url = record.url();
result["url"] = record.url();
References
  1. Use bracket notation for object property assignment. (link)

Comment thread src/tools/lookupRecord.ts
Comment on lines 221 to 223
if (record.comment && record.comment()) {
result.comment = record.comment();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Use bracket notation for property assignment.

Suggested change
if (record.comment && record.comment()) {
result.comment = record.comment();
}
if (record.comment && record.comment()) {
result["comment"] = record.comment();
}
References
  1. Use bracket notation for object property assignment. (link)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant