diff --git a/.changeset/dark-humans-jump.md b/.changeset/dark-humans-jump.md new file mode 100644 index 000000000..0b7d6aaed --- /dev/null +++ b/.changeset/dark-humans-jump.md @@ -0,0 +1,6 @@ +--- +"@inkeep/open-knowledge-server": patch +"@inkeep/open-knowledge": patch +--- + +Fix image and file paste/drop in the packaged desktop app, which failed with an "internal server error" toast (the upload endpoint returned HTTP 500). diff --git a/packages/cli/src/tsdown-bundle-coverage.test.ts b/packages/cli/src/tsdown-bundle-coverage.test.ts index fae261f8c..16b4abe7c 100644 --- a/packages/cli/src/tsdown-bundle-coverage.test.ts +++ b/packages/cli/src/tsdown-bundle-coverage.test.ts @@ -52,3 +52,30 @@ describe('tsdown alwaysBundle covers every cli runtime dep', () => { }); } }); + +describe('tsdown alwaysBundle covers the file-type transitive closure', () => { + const fileTypeClosure = [ + '@borewit/text-codec', + '@tokenizer/inflate', + '@tokenizer/token', + 'file-type', + 'ieee754', + 'strtok3', + 'token-types', + 'uint8array-extras', + ]; + + for (const dep of fileTypeClosure) { + test(`alwaysBundle covers transitive dep '${dep}'`, () => { + const escaped = dep.replace(/[\\^$*+?.()|[\]{}-]/g, '\\$&').replace(/\//g, '\\\\?/'); + const pattern = new RegExp(`\\^${escaped}\\(`); + expect( + pattern.test(alwaysBundleBlock), + `Add /^${dep}(\\/|$)/ to packages/cli/tsdown.config.ts \`alwaysBundle\`. ` + + `It is a pure-JS transitive dep of file-type (the server's upload ` + + `MIME-sniff); externalized, it leaves a bare \`import '${dep}'\` that ` + + `crashes packaged-app uploads with ERR_MODULE_NOT_FOUND.`, + ).toBe(true); + }); + } +}); diff --git a/packages/cli/tsdown.config.ts b/packages/cli/tsdown.config.ts index 193e919e6..d4ac39103 100644 --- a/packages/cli/tsdown.config.ts +++ b/packages/cli/tsdown.config.ts @@ -64,8 +64,13 @@ export default defineConfig({ /^@octokit\/auth-oauth-device(\/|$)/, /^@octokit\/request(\/|$)/, /^@octokit\/rest(\/|$)/, + /^@borewit\/text-codec(\/|$)/, + /^@tokenizer\/inflate(\/|$)/, + /^@tokenizer\/token(\/|$)/, /^cli-boxes(\/|$)/, /^commander(\/|$)/, + /^file-type(\/|$)/, + /^ieee754(\/|$)/, /^jsonc-parser(\/|$)/, /^just-bash(\/|$)/, /^picocolors(\/|$)/, @@ -75,6 +80,9 @@ export default defineConfig({ /^simple-git(\/|$)/, /^sirv(\/|$)/, /^smol-toml(\/|$)/, + /^strtok3(\/|$)/, + /^token-types(\/|$)/, + /^uint8array-extras(\/|$)/, /^yaml(\/|$)/, /^yazl(\/|$)/, /^zod(\/|$)/, diff --git a/packages/server/src/api-extension.ts b/packages/server/src/api-extension.ts index 441935212..ff2708efe 100644 --- a/packages/server/src/api-extension.ts +++ b/packages/server/src/api-extension.ts @@ -223,7 +223,7 @@ import { resolveGitDirDetailed, } from '@inkeep/open-knowledge-core/shadow-repo-layout'; import busboy from 'busboy'; -import { fileTypeFromFile } from 'file-type'; +import { fileTypeFromBuffer } from 'file-type'; import { parse as parseYaml } from 'yaml'; import { z } from 'zod'; import { captureEffect } from './activity-log.ts'; @@ -8318,12 +8318,13 @@ export function createApiExtension(options: ApiExtensionOptions): Extension { } } - const fileTypeResult = await fileTypeFromFile(tempPath); + const SNIFF_HEAD_BYTES = 4100; + const head = readTempFileHead(tempPath, SNIFF_HEAD_BYTES); + const fileTypeResult = await fileTypeFromBuffer(head); let detectedMime: string | undefined = fileTypeResult?.mime; let detectedExt: string | undefined = fileTypeResult?.ext; if (!detectedMime) { - const head = readTempFileHead(tempPath, 256); - const headText = head.toString('utf-8').replace(/^/, '').trimStart(); + const headText = head.subarray(0, 256).toString('utf-8').replace(/^/, '').trimStart(); if ( headText.startsWith('