@smartcat/sanity-plugin 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/_chunks-cjs/index.cjs +18 -5
  2. package/dist/_chunks-cjs/index.cjs.map +1 -1
  3. package/dist/_chunks-cjs/index2.cjs +8 -6
  4. package/dist/_chunks-cjs/index2.cjs.map +1 -1
  5. package/dist/_chunks-es/index.js +18 -5
  6. package/dist/_chunks-es/index.js.map +1 -1
  7. package/dist/_chunks-es/index2.js +8 -6
  8. package/dist/_chunks-es/index2.js.map +1 -1
  9. package/dist/export.cjs +21 -4
  10. package/dist/export.cjs.map +1 -1
  11. package/dist/export.d.cts +2 -0
  12. package/dist/export.d.ts +2 -0
  13. package/dist/export.js +22 -5
  14. package/dist/export.js.map +1 -1
  15. package/dist/index.cjs +156 -39
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.js +160 -43
  18. package/dist/index.js.map +1 -1
  19. package/dist/locjson.d.cts +15 -0
  20. package/dist/locjson.d.ts +15 -0
  21. package/dist/progress.d.cts +0 -19
  22. package/dist/progress.d.ts +0 -19
  23. package/dist/smartcat.cjs +14 -2
  24. package/dist/smartcat.cjs.map +1 -1
  25. package/dist/smartcat.d.cts +10 -1
  26. package/dist/smartcat.d.ts +10 -1
  27. package/dist/smartcat.js +14 -2
  28. package/dist/smartcat.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/actions/AddToProjectAction.tsx +23 -2
  31. package/src/export/export.test.ts +42 -5
  32. package/src/export/index.ts +63 -8
  33. package/src/lib/locjson/filename.ts +7 -2
  34. package/src/lib/locjson/locjson.test.ts +64 -0
  35. package/src/lib/locjson/serialize.ts +38 -2
  36. package/src/progress/core.ts +44 -3
  37. package/src/progress/progress.test.ts +40 -0
  38. package/src/schema/translationProject.ts +19 -0
  39. package/src/smartcat/client.ts +15 -1
  40. package/src/tool/ProjectView.tsx +108 -44
  41. package/src/tool/data.ts +7 -0
  42. package/src/tool/processing.ts +110 -4
@@ -14,6 +14,7 @@ export declare function buildLocjsonFilename(
14
14
  title?: string,
15
15
  options?: {
16
16
  draft?: boolean;
17
+ language?: string;
17
18
  },
18
19
  ): string;
19
20
 
@@ -208,6 +209,20 @@ export declare interface SerializeOptions {
208
209
  title?: string;
209
210
  types: string[];
210
211
  }) => void;
212
+ /**
213
+ * When set, produces a **bilingual** file for a single target language: each
214
+ * unit's `target` is populated from the existing translation, in parallel with
215
+ * its `source`. Used by the "send existing translations" flow. The target value
216
+ * of a field is read the same way as the source, but for the target language:
217
+ * - field-level: the target-language member's `value` on the same document;
218
+ * - document-level: the same field path on `doc` (the target-locale sibling).
219
+ * `language` is the Sanity locale (matches internationalized-array members).
220
+ */
221
+ target?: {
222
+ language: string;
223
+ /** The target-locale sibling document (document-level only). */
224
+ doc?: SerializableDocument;
225
+ };
211
226
  }
212
227
 
213
228
  /**
package/dist/locjson.d.ts CHANGED
@@ -14,6 +14,7 @@ export declare function buildLocjsonFilename(
14
14
  title?: string,
15
15
  options?: {
16
16
  draft?: boolean;
17
+ language?: string;
17
18
  },
18
19
  ): string;
19
20
 
@@ -208,6 +209,20 @@ export declare interface SerializeOptions {
208
209
  title?: string;
209
210
  types: string[];
210
211
  }) => void;
212
+ /**
213
+ * When set, produces a **bilingual** file for a single target language: each
214
+ * unit's `target` is populated from the existing translation, in parallel with
215
+ * its `source`. Used by the "send existing translations" flow. The target value
216
+ * of a field is read the same way as the source, but for the target language:
217
+ * - field-level: the target-language member's `value` on the same document;
218
+ * - document-level: the same field path on `doc` (the target-locale sibling).
219
+ * `language` is the Sanity locale (matches internationalized-array members).
220
+ */
221
+ target?: {
222
+ language: string;
223
+ /** The target-locale sibling document (document-level only). */
224
+ doc?: SerializableDocument;
225
+ };
211
226
  }
212
227
 
213
228
  /**
@@ -203,25 +203,6 @@ declare interface SmartcatWorkflowStage {
203
203
  totalWordsCount?: number;
204
204
  }
205
205
 
206
- /**
207
- * Extracts the 8-char source-id prefix Smartcat preserves in a document's
208
- * filename (built as `<title>-<idPrefix>.locjson`, or `<title>-<idPrefix>-draft.locjson`
209
- * when the content came from the document's draft). Used to correlate a Smartcat
210
- * document back to a Sanity source document.
211
- *
212
- * A trailing `-draft` marker is stripped first, so a draft-sourced file resolves
213
- * to the same id as its published counterpart. The prefix is then always the
214
- * final 8 characters before the (marker and) extension, so take the tail rather
215
- * than splitting on a dash — the title or the id prefix itself can
216
- * contain dashes (e.g. a custom id like `demo-pdpA` → prefix `demo-pdp`), which
217
- * would make a last-dash split return the wrong fragment.
218
- *
219
- * Only a literal `.locjson` extension is stripped (Smartcat's API often returns
220
- * the name without it). A generic strip-after-last-dot would eat the tail of any
221
- * DOTTED title — e.g. micro-copy keys like `aiAssistantModal.resetChat-11AKrV8G`
222
- * — leaving the wrong 8 chars, so those documents silently never correlate,
223
- * never import, and re-syncs duplicate them instead of updating.
224
- */
225
206
  export declare function sourceIdPrefix(doc: SmartcatDocument): string;
226
207
 
227
208
  /**
@@ -203,25 +203,6 @@ declare interface SmartcatWorkflowStage {
203
203
  totalWordsCount?: number;
204
204
  }
205
205
 
206
- /**
207
- * Extracts the 8-char source-id prefix Smartcat preserves in a document's
208
- * filename (built as `<title>-<idPrefix>.locjson`, or `<title>-<idPrefix>-draft.locjson`
209
- * when the content came from the document's draft). Used to correlate a Smartcat
210
- * document back to a Sanity source document.
211
- *
212
- * A trailing `-draft` marker is stripped first, so a draft-sourced file resolves
213
- * to the same id as its published counterpart. The prefix is then always the
214
- * final 8 characters before the (marker and) extension, so take the tail rather
215
- * than splitting on a dash — the title or the id prefix itself can
216
- * contain dashes (e.g. a custom id like `demo-pdpA` → prefix `demo-pdp`), which
217
- * would make a last-dash split return the wrong fragment.
218
- *
219
- * Only a literal `.locjson` extension is stripped (Smartcat's API often returns
220
- * the name without it). A generic strip-after-last-dot would eat the tail of any
221
- * DOTTED title — e.g. micro-copy keys like `aiAssistantModal.resetChat-11AKrV8G`
222
- * — leaving the wrong 8 chars, so those documents silently never correlate,
223
- * never import, and re-syncs duplicate them instead of updating.
224
- */
225
206
  export declare function sourceIdPrefix(doc: SmartcatDocument): string;
226
207
 
227
208
  /**
package/dist/smartcat.cjs CHANGED
@@ -116,9 +116,21 @@ class SmartcatClient {
116
116
  async getTemplates() {
117
117
  return this.request("GET", `${API_BASE}/template`);
118
118
  }
119
- /** Uploads one source file into a project. `filename` may include a folder path. */
120
- async uploadDocument(projectId, filename, content) {
119
+ /**
120
+ * Uploads one source file into a project. `filename` may include a folder path.
121
+ *
122
+ * When `targetLanguage` is given (a Smartcat language code), the file is
123
+ * restricted to that single target language via the `documentModel` settings
124
+ * part — used by the "send existing translations" flow, which uploads one
125
+ * bilingual LocJSON per language. Omitted ⇒ the document targets every project
126
+ * language (today's source-only default).
127
+ */
128
+ async uploadDocument(projectId, filename, content, targetLanguage) {
121
129
  const form = new FormData();
130
+ if (targetLanguage) {
131
+ const model = [{ targetLanguages: [targetLanguage] }];
132
+ form.append("documentModel", new Blob([JSON.stringify(model)], { type: "application/json" }), "documentModel");
133
+ }
122
134
  form.append("file", new Blob([content], { type: "application/octet-stream" }), filename);
123
135
  const result = await this.request(
124
136
  "POST",
@@ -1 +1 @@
1
- {"version":3,"file":"smartcat.cjs","sources":["../src/lib/zip.ts","../src/smartcat/client.ts"],"sourcesContent":["/**\n * Minimal ZIP reader for Smartcat batch-export archives.\n *\n * Dependency-free by design (like the rest of the Function-side code): built on\n * `DataView` + the global `DecompressionStream` (Node 18+ / browsers). Supports\n * the two compression methods Smartcat archives use — stored (0) and deflate (8).\n *\n * ZIP64 is REQUIRED, not an edge case: Smartcat writes export archives with\n * SharpZipLib's `UseZip64 = On` (forced), so central-directory size/offset\n * fields hold `0xFFFFFFFF` placeholders with the real 64-bit values in each\n * entry's ZIP64 extra field — even for tiny archives. Large archives\n * additionally use the ZIP64 end-of-central-directory record.\n */\n\nexport interface ZipEntry {\n /** Entry path inside the archive (folders separated by `/`). */\n name: string\n data: Uint8Array\n}\n\nconst LOCAL_HEADER_SIG = 0x04034b50\nconst CENTRAL_HEADER_SIG = 0x02014b50\nconst EOCD_SIG = 0x06054b50\nconst ZIP64_EOCD_SIG = 0x06064b50\nconst ZIP64_EOCD_LOCATOR_SIG = 0x07064b50\n/** EOCD record is 22 bytes + an optional comment of up to 65535 bytes. */\nconst EOCD_MIN_SIZE = 22\n/** ZIP64 EOCD locator is a fixed 20 bytes, sitting right before the classic EOCD. */\nconst ZIP64_LOCATOR_SIZE = 20\n/** Placeholder meaning \"the real value is in the ZIP64 extra field / record\". */\nconst U32_MAX = 0xffffffff\nconst U16_MAX = 0xffff\n/** Extra-field header id of the ZIP64 extended information block. */\nconst ZIP64_EXTRA_ID = 0x0001\n\n/** Whether the payload looks like a ZIP archive (`PK\\x03\\x04` magic). */\nexport function isZip(bytes: Uint8Array): boolean {\n return (\n bytes.length >= 4 &&\n bytes[0] === 0x50 &&\n bytes[1] === 0x4b &&\n bytes[2] === 0x03 &&\n bytes[3] === 0x04\n )\n}\n\n/** Extracts all file entries (directories are skipped). */\nexport async function unzip(bytes: Uint8Array): Promise<ZipEntry[]> {\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n const eocd = findEndOfCentralDirectory(view)\n const entries: ZipEntry[] = []\n\n let offset = eocd.centralDirectoryOffset\n for (let i = 0; i < eocd.entryCount; i++) {\n if (view.getUint32(offset, true) !== CENTRAL_HEADER_SIG) {\n throw new Error(`Invalid ZIP: bad central directory entry at ${offset}`)\n }\n const method = view.getUint16(offset + 10, true)\n let compressedSize: number = view.getUint32(offset + 20, true)\n let uncompressedSize: number = view.getUint32(offset + 24, true)\n const nameLength = view.getUint16(offset + 28, true)\n const extraLength = view.getUint16(offset + 30, true)\n const commentLength = view.getUint16(offset + 32, true)\n let localHeaderOffset: number = view.getUint32(offset + 42, true)\n const name = new TextDecoder().decode(bytes.subarray(offset + 46, offset + 46 + nameLength))\n\n // ZIP64: any field at its 32-bit max is a placeholder; the real value lives\n // in the entry's ZIP64 extra block, in a fixed order, holding ONLY the\n // fields that overflowed (Smartcat's forced-ZIP64 writer always defers the\n // sizes this way, even for small files).\n if (compressedSize === U32_MAX || uncompressedSize === U32_MAX || localHeaderOffset === U32_MAX) {\n const extraStart = offset + 46 + nameLength\n const extraEnd = extraStart + extraLength\n let p = extraStart\n while (p + 4 <= extraEnd) {\n const id = view.getUint16(p, true)\n const size = view.getUint16(p + 2, true)\n if (id === ZIP64_EXTRA_ID) {\n let q = p + 4\n if (uncompressedSize === U32_MAX) {\n uncompressedSize = Number(view.getBigUint64(q, true))\n q += 8\n }\n if (compressedSize === U32_MAX) {\n compressedSize = Number(view.getBigUint64(q, true))\n q += 8\n }\n if (localHeaderOffset === U32_MAX) {\n localHeaderOffset = Number(view.getBigUint64(q, true))\n }\n break\n }\n p += 4 + size\n }\n if (compressedSize === U32_MAX || localHeaderOffset === U32_MAX) {\n throw new Error(`Invalid ZIP: \"${name}\" needs ZIP64 sizes but has no ZIP64 extra field`)\n }\n }\n offset += 46 + nameLength + extraLength + commentLength\n\n if (name.endsWith('/')) continue // directory entry\n\n // The local header repeats name/extra with its own lengths; the data\n // follows it. Sizes come from the central directory (authoritative even\n // when the local header deferred them to a data descriptor).\n if (view.getUint32(localHeaderOffset, true) !== LOCAL_HEADER_SIG) {\n throw new Error(`Invalid ZIP: bad local header for \"${name}\"`)\n }\n const localNameLength = view.getUint16(localHeaderOffset + 26, true)\n const localExtraLength = view.getUint16(localHeaderOffset + 28, true)\n const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength\n const compressed = bytes.subarray(dataStart, dataStart + compressedSize)\n\n entries.push({name, data: await decompress(method, compressed, name)})\n }\n return entries\n}\n\n/** Convenience: unzip and decode every file entry as UTF-8 text. */\nexport async function unzipText(bytes: Uint8Array): Promise<{name: string; content: string}[]> {\n const entries = await unzip(bytes)\n const decoder = new TextDecoder()\n return entries.map((e) => ({name: e.name, content: decoder.decode(e.data)}))\n}\n\nfunction findEndOfCentralDirectory(view: DataView): {\n entryCount: number\n centralDirectoryOffset: number\n} {\n const lowest = Math.max(0, view.byteLength - EOCD_MIN_SIZE - 0xffff)\n for (let i = view.byteLength - EOCD_MIN_SIZE; i >= lowest; i--) {\n if (view.getUint32(i, true) !== EOCD_SIG) continue\n\n let entryCount: number = view.getUint16(i + 10, true)\n let centralDirectoryOffset: number = view.getUint32(i + 16, true)\n\n // ZIP64: placeholder values defer to the ZIP64 EOCD record, found via the\n // fixed-size locator that sits immediately before the classic EOCD.\n if (entryCount === U16_MAX || centralDirectoryOffset === U32_MAX) {\n const locator = i - ZIP64_LOCATOR_SIZE\n if (locator < 0 || view.getUint32(locator, true) !== ZIP64_EOCD_LOCATOR_SIG) {\n throw new Error('Invalid ZIP: ZIP64 values without a ZIP64 end-of-central-directory locator')\n }\n const zip64Eocd = Number(view.getBigUint64(locator + 8, true))\n if (view.getUint32(zip64Eocd, true) !== ZIP64_EOCD_SIG) {\n throw new Error('Invalid ZIP: bad ZIP64 end-of-central-directory record')\n }\n entryCount = Number(view.getBigUint64(zip64Eocd + 32, true))\n centralDirectoryOffset = Number(view.getBigUint64(zip64Eocd + 48, true))\n }\n return {entryCount, centralDirectoryOffset}\n }\n throw new Error('Invalid ZIP: end-of-central-directory record not found')\n}\n\nasync function decompress(method: number, data: Uint8Array, name: string): Promise<Uint8Array> {\n if (method === 0) return data // stored\n if (method === 8) {\n // Copy into a fresh ArrayBuffer so the Blob sees exactly this entry's bytes.\n const stream = new Blob([data.slice()]).stream().pipeThrough(new DecompressionStream('deflate-raw'))\n return new Uint8Array(await new Response(stream).arrayBuffer())\n }\n throw new Error(`Unsupported ZIP compression method ${method} for \"${name}\"`)\n}\n","import {isZip, unzipText} from '../lib/zip'\nimport type {\n CreateProjectInput,\n RequestLogger,\n SmartcatClientOptions,\n SmartcatDocument,\n SmartcatProject,\n SmartcatTemplatesResponse,\n} from './types'\n\nconst API_BASE = '/api/integration/v1'\n\n/** One file from a (batch) export: archive entry name + LocJSON text. */\nexport interface ExportedFile {\n /** Path inside the export archive; empty for a single-file response. */\n filename: string\n content: string\n}\n\n/** Error thrown when the Smartcat API returns a non-2xx response. */\nexport class SmartcatError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: string,\n ) {\n super(`Smartcat API error ${status}: ${body}`)\n this.name = 'SmartcatError'\n }\n}\n\n/** Error thrown when a request is aborted by the configured `requestTimeoutMs`. */\nexport class SmartcatTimeoutError extends Error {\n constructor(\n public readonly method: string,\n public readonly path: string,\n public readonly timeoutMs: number,\n ) {\n super(`Smartcat did not respond within ${formatDuration(timeoutMs)} for ${method} ${path}`)\n this.name = 'SmartcatTimeoutError'\n }\n}\n\n/** Human-readable duration, e.g. 570000 → \"9m30s\", 30000 → \"30s\". */\nfunction formatDuration(ms: number): string {\n const totalSeconds = Math.round(ms / 1000)\n const minutes = Math.floor(totalSeconds / 60)\n const seconds = totalSeconds % 60\n if (!minutes) return `${seconds}s`\n return seconds ? `${minutes}m${seconds}s` : `${minutes}m`\n}\n\n/** True for an abort/timeout rejection from `fetch` (DOMException-style). */\nfunction isAbortError(err: unknown): boolean {\n const name = (err as {name?: string} | undefined)?.name\n return name === 'TimeoutError' || name === 'AbortError'\n}\n\n/**\n * Minimal client for the Smartcat integration API.\n *\n * Uses Basic auth (`base64(workspaceId:apiKey)`). Built on global `fetch`,\n * `FormData` and `Blob` (Node 18+ / the Functions runtime); no Node-only APIs,\n * so it is portable. Secrets are required, so this only runs server-side.\n */\nexport class SmartcatClient {\n private readonly server: string\n private readonly authHeader: string\n private readonly fetchImpl: typeof fetch\n private readonly requestTimeoutMs?: number\n private requestLogger?: RequestLogger\n\n constructor(options: SmartcatClientOptions) {\n this.server = options.server.replace(/^https?:\\/\\//, '').replace(/\\/$/, '')\n this.authHeader = `Basic ${btoa(`${options.workspaceId}:${options.apiKey}`)}`\n this.fetchImpl = options.fetchImpl ?? fetch\n this.requestTimeoutMs = options.requestTimeoutMs\n }\n\n /** An `AbortSignal` that fires after `requestTimeoutMs`, or undefined if unset. */\n private timeoutSignal(): AbortSignal | undefined {\n return this.requestTimeoutMs ? AbortSignal.timeout(this.requestTimeoutMs) : undefined\n }\n\n /** Attach a sink that receives every HTTP call (method, path, status, body). */\n setRequestLogger(logger: RequestLogger): void {\n this.requestLogger = logger\n }\n\n /** Creates a project and returns it (including its generated `id` GUID). */\n async createProject(input: CreateProjectInput): Promise<SmartcatProject> {\n const {templateId, ...rest} = input\n // A template fully owns its workflow, MT/TM and pretranslation config, so we\n // send only the project identity + template id. The standard flow sets the\n // MT/TM/pretranslation flags here; `workflowStages` is supplied by the caller\n // (it's what distinguishes AI+human from AI-only).\n const body = templateId\n ? {assignToVendor: false, externalTag: 'smartcat-sanity-plugin', templateId, ...rest}\n : {\n assignToVendor: false,\n externalTag: 'smartcat-sanity-plugin',\n useMT: true,\n useTranslationMemory: true,\n pretranslate: true,\n autoPropagateRepetitions: true,\n minTranslationAssuranceLevel: 'regular',\n ...rest,\n }\n const form = new FormData()\n form.append('body', JSON.stringify(body))\n return this.request('POST', `${API_BASE}/project/create`, form)\n }\n\n /** Lists the workspace's project templates. */\n async getTemplates(): Promise<SmartcatTemplatesResponse> {\n return this.request('GET', `${API_BASE}/template`)\n }\n\n /** Uploads one source file into a project. `filename` may include a folder path. */\n async uploadDocument(\n projectId: string,\n filename: string,\n content: string,\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n // Use a generic content type: declaring application/json makes Smartcat try\n // to map the file to an API model instead of importing it as a document.\n form.append('file', new Blob([content], {type: 'application/octet-stream'}), filename)\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'POST',\n `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /**\n * Uploads many source files into a project in a single request — the\n * multi-file sibling of {@link uploadDocument}. Mirrors the reference\n * `AddDocuments` multipart contract: one `model` part carrying a JSON array\n * with one (default-settings) entry per file, plus one `file` part per file.\n * Returns the created documents (files × target languages).\n */\n async uploadDocuments(\n projectId: string,\n files: {filename: string; content: string}[],\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n // One settings object per file, positional with the file parts (empty = default\n // disassembly, as the single-file path). Correlation never relies on this order.\n form.append('model', new Blob([JSON.stringify(files.map(() => ({})))], {type: 'application/json'}), 'model')\n for (const f of files) {\n form.append('file', new Blob([f.content], {type: 'application/octet-stream'}), f.filename)\n }\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'POST',\n `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /**\n * Updates an existing document's source content by re-uploading the file.\n * Smartcat re-imports and merges, preserving translations of unchanged\n * segments. `documentId` is a target document id (`<fileGuid>_<langNum>`).\n */\n async updateDocument(\n documentId: string,\n filename: string,\n content: string,\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n form.append('file', new Blob([content], {type: 'application/octet-stream'}), filename)\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'PUT',\n `${API_BASE}/document/update?documentId=${encodeURIComponent(documentId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /** Fetches a project (including its documents and per-stage progress). */\n async getProject(projectId: string): Promise<SmartcatProject> {\n return this.request('GET', `${API_BASE}/project/${encodeURIComponent(projectId)}`)\n }\n\n /**\n * Deletes a document from its project. The id is a target document id\n * (`<fileGuid>_<langNum>`), but Smartcat removes the whole document — every\n * target language — in a single call.\n */\n async deleteDocument(documentId: string): Promise<void> {\n await this.request('DELETE', `${API_BASE}/document?documentIds=${encodeURIComponent(documentId)}`)\n }\n\n /** Builds the Smartcat web URL for a project's overview page. */\n projectUrl(projectId: string): string {\n return `https://${this.server}/projects/${projectId}/general`\n }\n\n /**\n * Exports a document's target (translated) content and returns it as text.\n * Two-step: request an export task, then poll until ready.\n *\n * `mode` defaults to `confirmed` — only segments confirmed at any workflow\n * stage are returned (unconfirmed/untranslated segments fall back to source),\n * so we never import unvetted machine-translation drafts. Use `current` to pull\n * the live state including unconfirmed edits.\n */\n async exportDocument(\n documentId: string,\n options: {\n mode?: 'confirmed' | 'current'\n pollIntervalMs?: number\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n } = {},\n ): Promise<string> {\n const {mode = 'confirmed', pollIntervalMs = 3000, maxAttempts = 20, sleep = defaultSleep} = options\n\n const task = await this.request<{id: string}>(\n 'POST',\n `${API_BASE}/document/export?documentIds=${encodeURIComponent(documentId)}&mode=${mode}&type=target`,\n )\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const {status, text} = await this.requestRaw('GET', `${API_BASE}/document/export/${task.id}`)\n if (status === 200) return text\n if (status === 204) {\n await sleep(pollIntervalMs)\n continue\n }\n throw new SmartcatError(status, text)\n }\n throw new Error(`Smartcat export for ${documentId} timed out`)\n }\n\n /**\n * Exports many documents' target content in **one** export task. Smartcat\n * fans the work out server-side and (for more than one document) returns a\n * ZIP archive of the individual files, so the per-document request+poll\n * round-trip is paid once per batch instead of once per document.\n *\n * Identity comes from each LocJSON's own `x-sanity` metadata, not the entry\n * name, so callers should correlate on the parsed content.\n */\n async exportDocumentsBatch(\n documentIds: string[],\n options: {\n mode?: 'confirmed' | 'current'\n pollIntervalMs?: number\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n } = {},\n ): Promise<ExportedFile[]> {\n if (documentIds.length === 0) return []\n // Bigger batches take longer to assemble server-side; poll longer, not faster.\n const {mode = 'confirmed', pollIntervalMs = 3000, maxAttempts = 60, sleep = defaultSleep} = options\n\n const query = documentIds.map((id) => `documentIds=${encodeURIComponent(id)}`).join('&')\n const task = await this.request<{id: string}>(\n 'POST',\n `${API_BASE}/document/export?${query}&mode=${mode}&type=target`,\n )\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const {status, bytes} = await this.requestBytes('GET', `${API_BASE}/document/export/${task.id}`)\n if (status === 200) {\n // A single-document task returns the bare file; multi-document tasks\n // return a ZIP. Decide by content, not by count — Smartcat may drop\n // documents it can't export from the archive.\n if (isZip(bytes)) {\n return (await unzipText(bytes)).map(({name, content}) => ({filename: name, content}))\n }\n return [{filename: '', content: new TextDecoder().decode(bytes)}]\n }\n if (status === 204) {\n await sleep(pollIntervalMs)\n continue\n }\n throw new SmartcatError(status, new TextDecoder().decode(bytes))\n }\n throw new Error(`Smartcat batch export of ${documentIds.length} document(s) timed out`)\n }\n\n private async request<T = unknown>(method: string, path: string, body?: BodyInit): Promise<T> {\n const {status, text} = await this.requestRaw(method, path, body)\n if (status < 200 || status >= 300) throw new SmartcatError(status, text)\n return (text ? JSON.parse(text) : null) as T\n }\n\n /**\n * Normalizes a `fetch` rejection. A timeout abort becomes a\n * {@link SmartcatTimeoutError} — traced through the request logger first (with\n * a synthetic status 0) so it surfaces in the same log as HTTP failures — while\n * anything else is rethrown unchanged.\n */\n private onFetchError(method: string, path: string, err: unknown): unknown {\n if (isAbortError(err) && this.requestTimeoutMs) {\n const timeout = new SmartcatTimeoutError(method, path, this.requestTimeoutMs)\n this.requestLogger?.({method, path, status: 0, body: timeout.message})\n return timeout\n }\n return err\n }\n\n private async requestRaw(\n method: string,\n path: string,\n body?: BodyInit,\n ): Promise<{status: number; text: string}> {\n let res: Response\n try {\n res = await this.fetchImpl(`https://${this.server}${path}`, {\n method,\n headers: {Authorization: this.authHeader},\n body,\n signal: this.timeoutSignal(),\n })\n } catch (err) {\n throw this.onFetchError(method, path, err)\n }\n const text = await res.text()\n this.requestLogger?.({method, path, status: res.status, body: text})\n return {status: res.status, text}\n }\n\n /** Like {@link requestRaw} but preserves the body as bytes (ZIP downloads). */\n private async requestBytes(method: string, path: string): Promise<{status: number; bytes: Uint8Array}> {\n let res: Response\n try {\n res = await this.fetchImpl(`https://${this.server}${path}`, {\n method,\n headers: {Authorization: this.authHeader},\n signal: this.timeoutSignal(),\n })\n } catch (err) {\n throw this.onFetchError(method, path, err)\n }\n const bytes = new Uint8Array(await res.arrayBuffer())\n // Log a size placeholder for successful binary payloads; error bodies are\n // text (Smartcat's error detail) and stay readable in the trace.\n const logBody = res.status >= 200 && res.status < 300 ? `<${bytes.length} bytes>` : new TextDecoder().decode(bytes)\n this.requestLogger?.({method, path, status: res.status, body: logBody})\n return {status: res.status, bytes}\n }\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"smartcat.cjs","sources":["../src/lib/zip.ts","../src/smartcat/client.ts"],"sourcesContent":["/**\n * Minimal ZIP reader for Smartcat batch-export archives.\n *\n * Dependency-free by design (like the rest of the Function-side code): built on\n * `DataView` + the global `DecompressionStream` (Node 18+ / browsers). Supports\n * the two compression methods Smartcat archives use — stored (0) and deflate (8).\n *\n * ZIP64 is REQUIRED, not an edge case: Smartcat writes export archives with\n * SharpZipLib's `UseZip64 = On` (forced), so central-directory size/offset\n * fields hold `0xFFFFFFFF` placeholders with the real 64-bit values in each\n * entry's ZIP64 extra field — even for tiny archives. Large archives\n * additionally use the ZIP64 end-of-central-directory record.\n */\n\nexport interface ZipEntry {\n /** Entry path inside the archive (folders separated by `/`). */\n name: string\n data: Uint8Array\n}\n\nconst LOCAL_HEADER_SIG = 0x04034b50\nconst CENTRAL_HEADER_SIG = 0x02014b50\nconst EOCD_SIG = 0x06054b50\nconst ZIP64_EOCD_SIG = 0x06064b50\nconst ZIP64_EOCD_LOCATOR_SIG = 0x07064b50\n/** EOCD record is 22 bytes + an optional comment of up to 65535 bytes. */\nconst EOCD_MIN_SIZE = 22\n/** ZIP64 EOCD locator is a fixed 20 bytes, sitting right before the classic EOCD. */\nconst ZIP64_LOCATOR_SIZE = 20\n/** Placeholder meaning \"the real value is in the ZIP64 extra field / record\". */\nconst U32_MAX = 0xffffffff\nconst U16_MAX = 0xffff\n/** Extra-field header id of the ZIP64 extended information block. */\nconst ZIP64_EXTRA_ID = 0x0001\n\n/** Whether the payload looks like a ZIP archive (`PK\\x03\\x04` magic). */\nexport function isZip(bytes: Uint8Array): boolean {\n return (\n bytes.length >= 4 &&\n bytes[0] === 0x50 &&\n bytes[1] === 0x4b &&\n bytes[2] === 0x03 &&\n bytes[3] === 0x04\n )\n}\n\n/** Extracts all file entries (directories are skipped). */\nexport async function unzip(bytes: Uint8Array): Promise<ZipEntry[]> {\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n const eocd = findEndOfCentralDirectory(view)\n const entries: ZipEntry[] = []\n\n let offset = eocd.centralDirectoryOffset\n for (let i = 0; i < eocd.entryCount; i++) {\n if (view.getUint32(offset, true) !== CENTRAL_HEADER_SIG) {\n throw new Error(`Invalid ZIP: bad central directory entry at ${offset}`)\n }\n const method = view.getUint16(offset + 10, true)\n let compressedSize: number = view.getUint32(offset + 20, true)\n let uncompressedSize: number = view.getUint32(offset + 24, true)\n const nameLength = view.getUint16(offset + 28, true)\n const extraLength = view.getUint16(offset + 30, true)\n const commentLength = view.getUint16(offset + 32, true)\n let localHeaderOffset: number = view.getUint32(offset + 42, true)\n const name = new TextDecoder().decode(bytes.subarray(offset + 46, offset + 46 + nameLength))\n\n // ZIP64: any field at its 32-bit max is a placeholder; the real value lives\n // in the entry's ZIP64 extra block, in a fixed order, holding ONLY the\n // fields that overflowed (Smartcat's forced-ZIP64 writer always defers the\n // sizes this way, even for small files).\n if (compressedSize === U32_MAX || uncompressedSize === U32_MAX || localHeaderOffset === U32_MAX) {\n const extraStart = offset + 46 + nameLength\n const extraEnd = extraStart + extraLength\n let p = extraStart\n while (p + 4 <= extraEnd) {\n const id = view.getUint16(p, true)\n const size = view.getUint16(p + 2, true)\n if (id === ZIP64_EXTRA_ID) {\n let q = p + 4\n if (uncompressedSize === U32_MAX) {\n uncompressedSize = Number(view.getBigUint64(q, true))\n q += 8\n }\n if (compressedSize === U32_MAX) {\n compressedSize = Number(view.getBigUint64(q, true))\n q += 8\n }\n if (localHeaderOffset === U32_MAX) {\n localHeaderOffset = Number(view.getBigUint64(q, true))\n }\n break\n }\n p += 4 + size\n }\n if (compressedSize === U32_MAX || localHeaderOffset === U32_MAX) {\n throw new Error(`Invalid ZIP: \"${name}\" needs ZIP64 sizes but has no ZIP64 extra field`)\n }\n }\n offset += 46 + nameLength + extraLength + commentLength\n\n if (name.endsWith('/')) continue // directory entry\n\n // The local header repeats name/extra with its own lengths; the data\n // follows it. Sizes come from the central directory (authoritative even\n // when the local header deferred them to a data descriptor).\n if (view.getUint32(localHeaderOffset, true) !== LOCAL_HEADER_SIG) {\n throw new Error(`Invalid ZIP: bad local header for \"${name}\"`)\n }\n const localNameLength = view.getUint16(localHeaderOffset + 26, true)\n const localExtraLength = view.getUint16(localHeaderOffset + 28, true)\n const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength\n const compressed = bytes.subarray(dataStart, dataStart + compressedSize)\n\n entries.push({name, data: await decompress(method, compressed, name)})\n }\n return entries\n}\n\n/** Convenience: unzip and decode every file entry as UTF-8 text. */\nexport async function unzipText(bytes: Uint8Array): Promise<{name: string; content: string}[]> {\n const entries = await unzip(bytes)\n const decoder = new TextDecoder()\n return entries.map((e) => ({name: e.name, content: decoder.decode(e.data)}))\n}\n\nfunction findEndOfCentralDirectory(view: DataView): {\n entryCount: number\n centralDirectoryOffset: number\n} {\n const lowest = Math.max(0, view.byteLength - EOCD_MIN_SIZE - 0xffff)\n for (let i = view.byteLength - EOCD_MIN_SIZE; i >= lowest; i--) {\n if (view.getUint32(i, true) !== EOCD_SIG) continue\n\n let entryCount: number = view.getUint16(i + 10, true)\n let centralDirectoryOffset: number = view.getUint32(i + 16, true)\n\n // ZIP64: placeholder values defer to the ZIP64 EOCD record, found via the\n // fixed-size locator that sits immediately before the classic EOCD.\n if (entryCount === U16_MAX || centralDirectoryOffset === U32_MAX) {\n const locator = i - ZIP64_LOCATOR_SIZE\n if (locator < 0 || view.getUint32(locator, true) !== ZIP64_EOCD_LOCATOR_SIG) {\n throw new Error('Invalid ZIP: ZIP64 values without a ZIP64 end-of-central-directory locator')\n }\n const zip64Eocd = Number(view.getBigUint64(locator + 8, true))\n if (view.getUint32(zip64Eocd, true) !== ZIP64_EOCD_SIG) {\n throw new Error('Invalid ZIP: bad ZIP64 end-of-central-directory record')\n }\n entryCount = Number(view.getBigUint64(zip64Eocd + 32, true))\n centralDirectoryOffset = Number(view.getBigUint64(zip64Eocd + 48, true))\n }\n return {entryCount, centralDirectoryOffset}\n }\n throw new Error('Invalid ZIP: end-of-central-directory record not found')\n}\n\nasync function decompress(method: number, data: Uint8Array, name: string): Promise<Uint8Array> {\n if (method === 0) return data // stored\n if (method === 8) {\n // Copy into a fresh ArrayBuffer so the Blob sees exactly this entry's bytes.\n const stream = new Blob([data.slice()]).stream().pipeThrough(new DecompressionStream('deflate-raw'))\n return new Uint8Array(await new Response(stream).arrayBuffer())\n }\n throw new Error(`Unsupported ZIP compression method ${method} for \"${name}\"`)\n}\n","import {isZip, unzipText} from '../lib/zip'\nimport type {\n CreateProjectInput,\n RequestLogger,\n SmartcatClientOptions,\n SmartcatDocument,\n SmartcatProject,\n SmartcatTemplatesResponse,\n} from './types'\n\nconst API_BASE = '/api/integration/v1'\n\n/** One file from a (batch) export: archive entry name + LocJSON text. */\nexport interface ExportedFile {\n /** Path inside the export archive; empty for a single-file response. */\n filename: string\n content: string\n}\n\n/** Error thrown when the Smartcat API returns a non-2xx response. */\nexport class SmartcatError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: string,\n ) {\n super(`Smartcat API error ${status}: ${body}`)\n this.name = 'SmartcatError'\n }\n}\n\n/** Error thrown when a request is aborted by the configured `requestTimeoutMs`. */\nexport class SmartcatTimeoutError extends Error {\n constructor(\n public readonly method: string,\n public readonly path: string,\n public readonly timeoutMs: number,\n ) {\n super(`Smartcat did not respond within ${formatDuration(timeoutMs)} for ${method} ${path}`)\n this.name = 'SmartcatTimeoutError'\n }\n}\n\n/** Human-readable duration, e.g. 570000 → \"9m30s\", 30000 → \"30s\". */\nfunction formatDuration(ms: number): string {\n const totalSeconds = Math.round(ms / 1000)\n const minutes = Math.floor(totalSeconds / 60)\n const seconds = totalSeconds % 60\n if (!minutes) return `${seconds}s`\n return seconds ? `${minutes}m${seconds}s` : `${minutes}m`\n}\n\n/** True for an abort/timeout rejection from `fetch` (DOMException-style). */\nfunction isAbortError(err: unknown): boolean {\n const name = (err as {name?: string} | undefined)?.name\n return name === 'TimeoutError' || name === 'AbortError'\n}\n\n/**\n * Minimal client for the Smartcat integration API.\n *\n * Uses Basic auth (`base64(workspaceId:apiKey)`). Built on global `fetch`,\n * `FormData` and `Blob` (Node 18+ / the Functions runtime); no Node-only APIs,\n * so it is portable. Secrets are required, so this only runs server-side.\n */\nexport class SmartcatClient {\n private readonly server: string\n private readonly authHeader: string\n private readonly fetchImpl: typeof fetch\n private readonly requestTimeoutMs?: number\n private requestLogger?: RequestLogger\n\n constructor(options: SmartcatClientOptions) {\n this.server = options.server.replace(/^https?:\\/\\//, '').replace(/\\/$/, '')\n this.authHeader = `Basic ${btoa(`${options.workspaceId}:${options.apiKey}`)}`\n this.fetchImpl = options.fetchImpl ?? fetch\n this.requestTimeoutMs = options.requestTimeoutMs\n }\n\n /** An `AbortSignal` that fires after `requestTimeoutMs`, or undefined if unset. */\n private timeoutSignal(): AbortSignal | undefined {\n return this.requestTimeoutMs ? AbortSignal.timeout(this.requestTimeoutMs) : undefined\n }\n\n /** Attach a sink that receives every HTTP call (method, path, status, body). */\n setRequestLogger(logger: RequestLogger): void {\n this.requestLogger = logger\n }\n\n /** Creates a project and returns it (including its generated `id` GUID). */\n async createProject(input: CreateProjectInput): Promise<SmartcatProject> {\n const {templateId, ...rest} = input\n // A template fully owns its workflow, MT/TM and pretranslation config, so we\n // send only the project identity + template id. The standard flow sets the\n // MT/TM/pretranslation flags here; `workflowStages` is supplied by the caller\n // (it's what distinguishes AI+human from AI-only).\n const body = templateId\n ? {assignToVendor: false, externalTag: 'smartcat-sanity-plugin', templateId, ...rest}\n : {\n assignToVendor: false,\n externalTag: 'smartcat-sanity-plugin',\n useMT: true,\n useTranslationMemory: true,\n pretranslate: true,\n autoPropagateRepetitions: true,\n minTranslationAssuranceLevel: 'regular',\n ...rest,\n }\n const form = new FormData()\n form.append('body', JSON.stringify(body))\n return this.request('POST', `${API_BASE}/project/create`, form)\n }\n\n /** Lists the workspace's project templates. */\n async getTemplates(): Promise<SmartcatTemplatesResponse> {\n return this.request('GET', `${API_BASE}/template`)\n }\n\n /**\n * Uploads one source file into a project. `filename` may include a folder path.\n *\n * When `targetLanguage` is given (a Smartcat language code), the file is\n * restricted to that single target language via the `documentModel` settings\n * part — used by the \"send existing translations\" flow, which uploads one\n * bilingual LocJSON per language. Omitted ⇒ the document targets every project\n * language (today's source-only default).\n */\n async uploadDocument(\n projectId: string,\n filename: string,\n content: string,\n targetLanguage?: string,\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n if (targetLanguage) {\n // `documentModel` is a JSON array (one entry per file); a bare object 400s.\n const model = [{targetLanguages: [targetLanguage]}]\n form.append('documentModel', new Blob([JSON.stringify(model)], {type: 'application/json'}), 'documentModel')\n }\n // Use a generic content type: declaring application/json makes Smartcat try\n // to map the file to an API model instead of importing it as a document.\n form.append('file', new Blob([content], {type: 'application/octet-stream'}), filename)\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'POST',\n `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /**\n * Uploads many source files into a project in a single request — the\n * multi-file sibling of {@link uploadDocument}. Mirrors the reference\n * `AddDocuments` multipart contract: one `model` part carrying a JSON array\n * with one (default-settings) entry per file, plus one `file` part per file.\n * Returns the created documents (files × target languages).\n */\n async uploadDocuments(\n projectId: string,\n files: {filename: string; content: string}[],\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n // One settings object per file, positional with the file parts (empty = default\n // disassembly, as the single-file path). Correlation never relies on this order.\n form.append('model', new Blob([JSON.stringify(files.map(() => ({})))], {type: 'application/json'}), 'model')\n for (const f of files) {\n form.append('file', new Blob([f.content], {type: 'application/octet-stream'}), f.filename)\n }\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'POST',\n `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /**\n * Updates an existing document's source content by re-uploading the file.\n * Smartcat re-imports and merges, preserving translations of unchanged\n * segments. `documentId` is a target document id (`<fileGuid>_<langNum>`).\n */\n async updateDocument(\n documentId: string,\n filename: string,\n content: string,\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n form.append('file', new Blob([content], {type: 'application/octet-stream'}), filename)\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'PUT',\n `${API_BASE}/document/update?documentId=${encodeURIComponent(documentId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /** Fetches a project (including its documents and per-stage progress). */\n async getProject(projectId: string): Promise<SmartcatProject> {\n return this.request('GET', `${API_BASE}/project/${encodeURIComponent(projectId)}`)\n }\n\n /**\n * Deletes a document from its project. The id is a target document id\n * (`<fileGuid>_<langNum>`), but Smartcat removes the whole document — every\n * target language — in a single call.\n */\n async deleteDocument(documentId: string): Promise<void> {\n await this.request('DELETE', `${API_BASE}/document?documentIds=${encodeURIComponent(documentId)}`)\n }\n\n /** Builds the Smartcat web URL for a project's overview page. */\n projectUrl(projectId: string): string {\n return `https://${this.server}/projects/${projectId}/general`\n }\n\n /**\n * Exports a document's target (translated) content and returns it as text.\n * Two-step: request an export task, then poll until ready.\n *\n * `mode` defaults to `confirmed` — only segments confirmed at any workflow\n * stage are returned (unconfirmed/untranslated segments fall back to source),\n * so we never import unvetted machine-translation drafts. Use `current` to pull\n * the live state including unconfirmed edits.\n */\n async exportDocument(\n documentId: string,\n options: {\n mode?: 'confirmed' | 'current'\n pollIntervalMs?: number\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n } = {},\n ): Promise<string> {\n const {mode = 'confirmed', pollIntervalMs = 3000, maxAttempts = 20, sleep = defaultSleep} = options\n\n const task = await this.request<{id: string}>(\n 'POST',\n `${API_BASE}/document/export?documentIds=${encodeURIComponent(documentId)}&mode=${mode}&type=target`,\n )\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const {status, text} = await this.requestRaw('GET', `${API_BASE}/document/export/${task.id}`)\n if (status === 200) return text\n if (status === 204) {\n await sleep(pollIntervalMs)\n continue\n }\n throw new SmartcatError(status, text)\n }\n throw new Error(`Smartcat export for ${documentId} timed out`)\n }\n\n /**\n * Exports many documents' target content in **one** export task. Smartcat\n * fans the work out server-side and (for more than one document) returns a\n * ZIP archive of the individual files, so the per-document request+poll\n * round-trip is paid once per batch instead of once per document.\n *\n * Identity comes from each LocJSON's own `x-sanity` metadata, not the entry\n * name, so callers should correlate on the parsed content.\n */\n async exportDocumentsBatch(\n documentIds: string[],\n options: {\n mode?: 'confirmed' | 'current'\n pollIntervalMs?: number\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n } = {},\n ): Promise<ExportedFile[]> {\n if (documentIds.length === 0) return []\n // Bigger batches take longer to assemble server-side; poll longer, not faster.\n const {mode = 'confirmed', pollIntervalMs = 3000, maxAttempts = 60, sleep = defaultSleep} = options\n\n const query = documentIds.map((id) => `documentIds=${encodeURIComponent(id)}`).join('&')\n const task = await this.request<{id: string}>(\n 'POST',\n `${API_BASE}/document/export?${query}&mode=${mode}&type=target`,\n )\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const {status, bytes} = await this.requestBytes('GET', `${API_BASE}/document/export/${task.id}`)\n if (status === 200) {\n // A single-document task returns the bare file; multi-document tasks\n // return a ZIP. Decide by content, not by count — Smartcat may drop\n // documents it can't export from the archive.\n if (isZip(bytes)) {\n return (await unzipText(bytes)).map(({name, content}) => ({filename: name, content}))\n }\n return [{filename: '', content: new TextDecoder().decode(bytes)}]\n }\n if (status === 204) {\n await sleep(pollIntervalMs)\n continue\n }\n throw new SmartcatError(status, new TextDecoder().decode(bytes))\n }\n throw new Error(`Smartcat batch export of ${documentIds.length} document(s) timed out`)\n }\n\n private async request<T = unknown>(method: string, path: string, body?: BodyInit): Promise<T> {\n const {status, text} = await this.requestRaw(method, path, body)\n if (status < 200 || status >= 300) throw new SmartcatError(status, text)\n return (text ? JSON.parse(text) : null) as T\n }\n\n /**\n * Normalizes a `fetch` rejection. A timeout abort becomes a\n * {@link SmartcatTimeoutError} — traced through the request logger first (with\n * a synthetic status 0) so it surfaces in the same log as HTTP failures — while\n * anything else is rethrown unchanged.\n */\n private onFetchError(method: string, path: string, err: unknown): unknown {\n if (isAbortError(err) && this.requestTimeoutMs) {\n const timeout = new SmartcatTimeoutError(method, path, this.requestTimeoutMs)\n this.requestLogger?.({method, path, status: 0, body: timeout.message})\n return timeout\n }\n return err\n }\n\n private async requestRaw(\n method: string,\n path: string,\n body?: BodyInit,\n ): Promise<{status: number; text: string}> {\n let res: Response\n try {\n res = await this.fetchImpl(`https://${this.server}${path}`, {\n method,\n headers: {Authorization: this.authHeader},\n body,\n signal: this.timeoutSignal(),\n })\n } catch (err) {\n throw this.onFetchError(method, path, err)\n }\n const text = await res.text()\n this.requestLogger?.({method, path, status: res.status, body: text})\n return {status: res.status, text}\n }\n\n /** Like {@link requestRaw} but preserves the body as bytes (ZIP downloads). */\n private async requestBytes(method: string, path: string): Promise<{status: number; bytes: Uint8Array}> {\n let res: Response\n try {\n res = await this.fetchImpl(`https://${this.server}${path}`, {\n method,\n headers: {Authorization: this.authHeader},\n signal: this.timeoutSignal(),\n })\n } catch (err) {\n throw this.onFetchError(method, path, err)\n }\n const bytes = new Uint8Array(await res.arrayBuffer())\n // Log a size placeholder for successful binary payloads; error bodies are\n // text (Smartcat's error detail) and stay readable in the trace.\n const logBody = res.status >= 200 && res.status < 300 ? `<${bytes.length} bytes>` : new TextDecoder().decode(bytes)\n this.requestLogger?.({method, path, status: res.status, body: logBody})\n return {status: res.status, bytes}\n }\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n"],"names":[],"mappings":"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"}
@@ -57,11 +57,20 @@ export declare class SmartcatClient {
57
57
  createProject(input: CreateProjectInput): Promise<SmartcatProject>;
58
58
  /** Lists the workspace's project templates. */
59
59
  getTemplates(): Promise<SmartcatTemplatesResponse>;
60
- /** Uploads one source file into a project. `filename` may include a folder path. */
60
+ /**
61
+ * Uploads one source file into a project. `filename` may include a folder path.
62
+ *
63
+ * When `targetLanguage` is given (a Smartcat language code), the file is
64
+ * restricted to that single target language via the `documentModel` settings
65
+ * part — used by the "send existing translations" flow, which uploads one
66
+ * bilingual LocJSON per language. Omitted ⇒ the document targets every project
67
+ * language (today's source-only default).
68
+ */
61
69
  uploadDocument(
62
70
  projectId: string,
63
71
  filename: string,
64
72
  content: string,
73
+ targetLanguage?: string,
65
74
  ): Promise<SmartcatDocument[]>;
66
75
  /**
67
76
  * Uploads many source files into a project in a single request — the
@@ -57,11 +57,20 @@ export declare class SmartcatClient {
57
57
  createProject(input: CreateProjectInput): Promise<SmartcatProject>;
58
58
  /** Lists the workspace's project templates. */
59
59
  getTemplates(): Promise<SmartcatTemplatesResponse>;
60
- /** Uploads one source file into a project. `filename` may include a folder path. */
60
+ /**
61
+ * Uploads one source file into a project. `filename` may include a folder path.
62
+ *
63
+ * When `targetLanguage` is given (a Smartcat language code), the file is
64
+ * restricted to that single target language via the `documentModel` settings
65
+ * part — used by the "send existing translations" flow, which uploads one
66
+ * bilingual LocJSON per language. Omitted ⇒ the document targets every project
67
+ * language (today's source-only default).
68
+ */
61
69
  uploadDocument(
62
70
  projectId: string,
63
71
  filename: string,
64
72
  content: string,
73
+ targetLanguage?: string,
65
74
  ): Promise<SmartcatDocument[]>;
66
75
  /**
67
76
  * Uploads many source files into a project in a single request — the
package/dist/smartcat.js CHANGED
@@ -114,9 +114,21 @@ class SmartcatClient {
114
114
  async getTemplates() {
115
115
  return this.request("GET", `${API_BASE}/template`);
116
116
  }
117
- /** Uploads one source file into a project. `filename` may include a folder path. */
118
- async uploadDocument(projectId, filename, content) {
117
+ /**
118
+ * Uploads one source file into a project. `filename` may include a folder path.
119
+ *
120
+ * When `targetLanguage` is given (a Smartcat language code), the file is
121
+ * restricted to that single target language via the `documentModel` settings
122
+ * part — used by the "send existing translations" flow, which uploads one
123
+ * bilingual LocJSON per language. Omitted ⇒ the document targets every project
124
+ * language (today's source-only default).
125
+ */
126
+ async uploadDocument(projectId, filename, content, targetLanguage) {
119
127
  const form = new FormData();
128
+ if (targetLanguage) {
129
+ const model = [{ targetLanguages: [targetLanguage] }];
130
+ form.append("documentModel", new Blob([JSON.stringify(model)], { type: "application/json" }), "documentModel");
131
+ }
120
132
  form.append("file", new Blob([content], { type: "application/octet-stream" }), filename);
121
133
  const result = await this.request(
122
134
  "POST",
@@ -1 +1 @@
1
- {"version":3,"file":"smartcat.js","sources":["../src/lib/zip.ts","../src/smartcat/client.ts"],"sourcesContent":["/**\n * Minimal ZIP reader for Smartcat batch-export archives.\n *\n * Dependency-free by design (like the rest of the Function-side code): built on\n * `DataView` + the global `DecompressionStream` (Node 18+ / browsers). Supports\n * the two compression methods Smartcat archives use — stored (0) and deflate (8).\n *\n * ZIP64 is REQUIRED, not an edge case: Smartcat writes export archives with\n * SharpZipLib's `UseZip64 = On` (forced), so central-directory size/offset\n * fields hold `0xFFFFFFFF` placeholders with the real 64-bit values in each\n * entry's ZIP64 extra field — even for tiny archives. Large archives\n * additionally use the ZIP64 end-of-central-directory record.\n */\n\nexport interface ZipEntry {\n /** Entry path inside the archive (folders separated by `/`). */\n name: string\n data: Uint8Array\n}\n\nconst LOCAL_HEADER_SIG = 0x04034b50\nconst CENTRAL_HEADER_SIG = 0x02014b50\nconst EOCD_SIG = 0x06054b50\nconst ZIP64_EOCD_SIG = 0x06064b50\nconst ZIP64_EOCD_LOCATOR_SIG = 0x07064b50\n/** EOCD record is 22 bytes + an optional comment of up to 65535 bytes. */\nconst EOCD_MIN_SIZE = 22\n/** ZIP64 EOCD locator is a fixed 20 bytes, sitting right before the classic EOCD. */\nconst ZIP64_LOCATOR_SIZE = 20\n/** Placeholder meaning \"the real value is in the ZIP64 extra field / record\". */\nconst U32_MAX = 0xffffffff\nconst U16_MAX = 0xffff\n/** Extra-field header id of the ZIP64 extended information block. */\nconst ZIP64_EXTRA_ID = 0x0001\n\n/** Whether the payload looks like a ZIP archive (`PK\\x03\\x04` magic). */\nexport function isZip(bytes: Uint8Array): boolean {\n return (\n bytes.length >= 4 &&\n bytes[0] === 0x50 &&\n bytes[1] === 0x4b &&\n bytes[2] === 0x03 &&\n bytes[3] === 0x04\n )\n}\n\n/** Extracts all file entries (directories are skipped). */\nexport async function unzip(bytes: Uint8Array): Promise<ZipEntry[]> {\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n const eocd = findEndOfCentralDirectory(view)\n const entries: ZipEntry[] = []\n\n let offset = eocd.centralDirectoryOffset\n for (let i = 0; i < eocd.entryCount; i++) {\n if (view.getUint32(offset, true) !== CENTRAL_HEADER_SIG) {\n throw new Error(`Invalid ZIP: bad central directory entry at ${offset}`)\n }\n const method = view.getUint16(offset + 10, true)\n let compressedSize: number = view.getUint32(offset + 20, true)\n let uncompressedSize: number = view.getUint32(offset + 24, true)\n const nameLength = view.getUint16(offset + 28, true)\n const extraLength = view.getUint16(offset + 30, true)\n const commentLength = view.getUint16(offset + 32, true)\n let localHeaderOffset: number = view.getUint32(offset + 42, true)\n const name = new TextDecoder().decode(bytes.subarray(offset + 46, offset + 46 + nameLength))\n\n // ZIP64: any field at its 32-bit max is a placeholder; the real value lives\n // in the entry's ZIP64 extra block, in a fixed order, holding ONLY the\n // fields that overflowed (Smartcat's forced-ZIP64 writer always defers the\n // sizes this way, even for small files).\n if (compressedSize === U32_MAX || uncompressedSize === U32_MAX || localHeaderOffset === U32_MAX) {\n const extraStart = offset + 46 + nameLength\n const extraEnd = extraStart + extraLength\n let p = extraStart\n while (p + 4 <= extraEnd) {\n const id = view.getUint16(p, true)\n const size = view.getUint16(p + 2, true)\n if (id === ZIP64_EXTRA_ID) {\n let q = p + 4\n if (uncompressedSize === U32_MAX) {\n uncompressedSize = Number(view.getBigUint64(q, true))\n q += 8\n }\n if (compressedSize === U32_MAX) {\n compressedSize = Number(view.getBigUint64(q, true))\n q += 8\n }\n if (localHeaderOffset === U32_MAX) {\n localHeaderOffset = Number(view.getBigUint64(q, true))\n }\n break\n }\n p += 4 + size\n }\n if (compressedSize === U32_MAX || localHeaderOffset === U32_MAX) {\n throw new Error(`Invalid ZIP: \"${name}\" needs ZIP64 sizes but has no ZIP64 extra field`)\n }\n }\n offset += 46 + nameLength + extraLength + commentLength\n\n if (name.endsWith('/')) continue // directory entry\n\n // The local header repeats name/extra with its own lengths; the data\n // follows it. Sizes come from the central directory (authoritative even\n // when the local header deferred them to a data descriptor).\n if (view.getUint32(localHeaderOffset, true) !== LOCAL_HEADER_SIG) {\n throw new Error(`Invalid ZIP: bad local header for \"${name}\"`)\n }\n const localNameLength = view.getUint16(localHeaderOffset + 26, true)\n const localExtraLength = view.getUint16(localHeaderOffset + 28, true)\n const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength\n const compressed = bytes.subarray(dataStart, dataStart + compressedSize)\n\n entries.push({name, data: await decompress(method, compressed, name)})\n }\n return entries\n}\n\n/** Convenience: unzip and decode every file entry as UTF-8 text. */\nexport async function unzipText(bytes: Uint8Array): Promise<{name: string; content: string}[]> {\n const entries = await unzip(bytes)\n const decoder = new TextDecoder()\n return entries.map((e) => ({name: e.name, content: decoder.decode(e.data)}))\n}\n\nfunction findEndOfCentralDirectory(view: DataView): {\n entryCount: number\n centralDirectoryOffset: number\n} {\n const lowest = Math.max(0, view.byteLength - EOCD_MIN_SIZE - 0xffff)\n for (let i = view.byteLength - EOCD_MIN_SIZE; i >= lowest; i--) {\n if (view.getUint32(i, true) !== EOCD_SIG) continue\n\n let entryCount: number = view.getUint16(i + 10, true)\n let centralDirectoryOffset: number = view.getUint32(i + 16, true)\n\n // ZIP64: placeholder values defer to the ZIP64 EOCD record, found via the\n // fixed-size locator that sits immediately before the classic EOCD.\n if (entryCount === U16_MAX || centralDirectoryOffset === U32_MAX) {\n const locator = i - ZIP64_LOCATOR_SIZE\n if (locator < 0 || view.getUint32(locator, true) !== ZIP64_EOCD_LOCATOR_SIG) {\n throw new Error('Invalid ZIP: ZIP64 values without a ZIP64 end-of-central-directory locator')\n }\n const zip64Eocd = Number(view.getBigUint64(locator + 8, true))\n if (view.getUint32(zip64Eocd, true) !== ZIP64_EOCD_SIG) {\n throw new Error('Invalid ZIP: bad ZIP64 end-of-central-directory record')\n }\n entryCount = Number(view.getBigUint64(zip64Eocd + 32, true))\n centralDirectoryOffset = Number(view.getBigUint64(zip64Eocd + 48, true))\n }\n return {entryCount, centralDirectoryOffset}\n }\n throw new Error('Invalid ZIP: end-of-central-directory record not found')\n}\n\nasync function decompress(method: number, data: Uint8Array, name: string): Promise<Uint8Array> {\n if (method === 0) return data // stored\n if (method === 8) {\n // Copy into a fresh ArrayBuffer so the Blob sees exactly this entry's bytes.\n const stream = new Blob([data.slice()]).stream().pipeThrough(new DecompressionStream('deflate-raw'))\n return new Uint8Array(await new Response(stream).arrayBuffer())\n }\n throw new Error(`Unsupported ZIP compression method ${method} for \"${name}\"`)\n}\n","import {isZip, unzipText} from '../lib/zip'\nimport type {\n CreateProjectInput,\n RequestLogger,\n SmartcatClientOptions,\n SmartcatDocument,\n SmartcatProject,\n SmartcatTemplatesResponse,\n} from './types'\n\nconst API_BASE = '/api/integration/v1'\n\n/** One file from a (batch) export: archive entry name + LocJSON text. */\nexport interface ExportedFile {\n /** Path inside the export archive; empty for a single-file response. */\n filename: string\n content: string\n}\n\n/** Error thrown when the Smartcat API returns a non-2xx response. */\nexport class SmartcatError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: string,\n ) {\n super(`Smartcat API error ${status}: ${body}`)\n this.name = 'SmartcatError'\n }\n}\n\n/** Error thrown when a request is aborted by the configured `requestTimeoutMs`. */\nexport class SmartcatTimeoutError extends Error {\n constructor(\n public readonly method: string,\n public readonly path: string,\n public readonly timeoutMs: number,\n ) {\n super(`Smartcat did not respond within ${formatDuration(timeoutMs)} for ${method} ${path}`)\n this.name = 'SmartcatTimeoutError'\n }\n}\n\n/** Human-readable duration, e.g. 570000 → \"9m30s\", 30000 → \"30s\". */\nfunction formatDuration(ms: number): string {\n const totalSeconds = Math.round(ms / 1000)\n const minutes = Math.floor(totalSeconds / 60)\n const seconds = totalSeconds % 60\n if (!minutes) return `${seconds}s`\n return seconds ? `${minutes}m${seconds}s` : `${minutes}m`\n}\n\n/** True for an abort/timeout rejection from `fetch` (DOMException-style). */\nfunction isAbortError(err: unknown): boolean {\n const name = (err as {name?: string} | undefined)?.name\n return name === 'TimeoutError' || name === 'AbortError'\n}\n\n/**\n * Minimal client for the Smartcat integration API.\n *\n * Uses Basic auth (`base64(workspaceId:apiKey)`). Built on global `fetch`,\n * `FormData` and `Blob` (Node 18+ / the Functions runtime); no Node-only APIs,\n * so it is portable. Secrets are required, so this only runs server-side.\n */\nexport class SmartcatClient {\n private readonly server: string\n private readonly authHeader: string\n private readonly fetchImpl: typeof fetch\n private readonly requestTimeoutMs?: number\n private requestLogger?: RequestLogger\n\n constructor(options: SmartcatClientOptions) {\n this.server = options.server.replace(/^https?:\\/\\//, '').replace(/\\/$/, '')\n this.authHeader = `Basic ${btoa(`${options.workspaceId}:${options.apiKey}`)}`\n this.fetchImpl = options.fetchImpl ?? fetch\n this.requestTimeoutMs = options.requestTimeoutMs\n }\n\n /** An `AbortSignal` that fires after `requestTimeoutMs`, or undefined if unset. */\n private timeoutSignal(): AbortSignal | undefined {\n return this.requestTimeoutMs ? AbortSignal.timeout(this.requestTimeoutMs) : undefined\n }\n\n /** Attach a sink that receives every HTTP call (method, path, status, body). */\n setRequestLogger(logger: RequestLogger): void {\n this.requestLogger = logger\n }\n\n /** Creates a project and returns it (including its generated `id` GUID). */\n async createProject(input: CreateProjectInput): Promise<SmartcatProject> {\n const {templateId, ...rest} = input\n // A template fully owns its workflow, MT/TM and pretranslation config, so we\n // send only the project identity + template id. The standard flow sets the\n // MT/TM/pretranslation flags here; `workflowStages` is supplied by the caller\n // (it's what distinguishes AI+human from AI-only).\n const body = templateId\n ? {assignToVendor: false, externalTag: 'smartcat-sanity-plugin', templateId, ...rest}\n : {\n assignToVendor: false,\n externalTag: 'smartcat-sanity-plugin',\n useMT: true,\n useTranslationMemory: true,\n pretranslate: true,\n autoPropagateRepetitions: true,\n minTranslationAssuranceLevel: 'regular',\n ...rest,\n }\n const form = new FormData()\n form.append('body', JSON.stringify(body))\n return this.request('POST', `${API_BASE}/project/create`, form)\n }\n\n /** Lists the workspace's project templates. */\n async getTemplates(): Promise<SmartcatTemplatesResponse> {\n return this.request('GET', `${API_BASE}/template`)\n }\n\n /** Uploads one source file into a project. `filename` may include a folder path. */\n async uploadDocument(\n projectId: string,\n filename: string,\n content: string,\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n // Use a generic content type: declaring application/json makes Smartcat try\n // to map the file to an API model instead of importing it as a document.\n form.append('file', new Blob([content], {type: 'application/octet-stream'}), filename)\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'POST',\n `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /**\n * Uploads many source files into a project in a single request — the\n * multi-file sibling of {@link uploadDocument}. Mirrors the reference\n * `AddDocuments` multipart contract: one `model` part carrying a JSON array\n * with one (default-settings) entry per file, plus one `file` part per file.\n * Returns the created documents (files × target languages).\n */\n async uploadDocuments(\n projectId: string,\n files: {filename: string; content: string}[],\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n // One settings object per file, positional with the file parts (empty = default\n // disassembly, as the single-file path). Correlation never relies on this order.\n form.append('model', new Blob([JSON.stringify(files.map(() => ({})))], {type: 'application/json'}), 'model')\n for (const f of files) {\n form.append('file', new Blob([f.content], {type: 'application/octet-stream'}), f.filename)\n }\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'POST',\n `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /**\n * Updates an existing document's source content by re-uploading the file.\n * Smartcat re-imports and merges, preserving translations of unchanged\n * segments. `documentId` is a target document id (`<fileGuid>_<langNum>`).\n */\n async updateDocument(\n documentId: string,\n filename: string,\n content: string,\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n form.append('file', new Blob([content], {type: 'application/octet-stream'}), filename)\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'PUT',\n `${API_BASE}/document/update?documentId=${encodeURIComponent(documentId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /** Fetches a project (including its documents and per-stage progress). */\n async getProject(projectId: string): Promise<SmartcatProject> {\n return this.request('GET', `${API_BASE}/project/${encodeURIComponent(projectId)}`)\n }\n\n /**\n * Deletes a document from its project. The id is a target document id\n * (`<fileGuid>_<langNum>`), but Smartcat removes the whole document — every\n * target language — in a single call.\n */\n async deleteDocument(documentId: string): Promise<void> {\n await this.request('DELETE', `${API_BASE}/document?documentIds=${encodeURIComponent(documentId)}`)\n }\n\n /** Builds the Smartcat web URL for a project's overview page. */\n projectUrl(projectId: string): string {\n return `https://${this.server}/projects/${projectId}/general`\n }\n\n /**\n * Exports a document's target (translated) content and returns it as text.\n * Two-step: request an export task, then poll until ready.\n *\n * `mode` defaults to `confirmed` — only segments confirmed at any workflow\n * stage are returned (unconfirmed/untranslated segments fall back to source),\n * so we never import unvetted machine-translation drafts. Use `current` to pull\n * the live state including unconfirmed edits.\n */\n async exportDocument(\n documentId: string,\n options: {\n mode?: 'confirmed' | 'current'\n pollIntervalMs?: number\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n } = {},\n ): Promise<string> {\n const {mode = 'confirmed', pollIntervalMs = 3000, maxAttempts = 20, sleep = defaultSleep} = options\n\n const task = await this.request<{id: string}>(\n 'POST',\n `${API_BASE}/document/export?documentIds=${encodeURIComponent(documentId)}&mode=${mode}&type=target`,\n )\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const {status, text} = await this.requestRaw('GET', `${API_BASE}/document/export/${task.id}`)\n if (status === 200) return text\n if (status === 204) {\n await sleep(pollIntervalMs)\n continue\n }\n throw new SmartcatError(status, text)\n }\n throw new Error(`Smartcat export for ${documentId} timed out`)\n }\n\n /**\n * Exports many documents' target content in **one** export task. Smartcat\n * fans the work out server-side and (for more than one document) returns a\n * ZIP archive of the individual files, so the per-document request+poll\n * round-trip is paid once per batch instead of once per document.\n *\n * Identity comes from each LocJSON's own `x-sanity` metadata, not the entry\n * name, so callers should correlate on the parsed content.\n */\n async exportDocumentsBatch(\n documentIds: string[],\n options: {\n mode?: 'confirmed' | 'current'\n pollIntervalMs?: number\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n } = {},\n ): Promise<ExportedFile[]> {\n if (documentIds.length === 0) return []\n // Bigger batches take longer to assemble server-side; poll longer, not faster.\n const {mode = 'confirmed', pollIntervalMs = 3000, maxAttempts = 60, sleep = defaultSleep} = options\n\n const query = documentIds.map((id) => `documentIds=${encodeURIComponent(id)}`).join('&')\n const task = await this.request<{id: string}>(\n 'POST',\n `${API_BASE}/document/export?${query}&mode=${mode}&type=target`,\n )\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const {status, bytes} = await this.requestBytes('GET', `${API_BASE}/document/export/${task.id}`)\n if (status === 200) {\n // A single-document task returns the bare file; multi-document tasks\n // return a ZIP. Decide by content, not by count — Smartcat may drop\n // documents it can't export from the archive.\n if (isZip(bytes)) {\n return (await unzipText(bytes)).map(({name, content}) => ({filename: name, content}))\n }\n return [{filename: '', content: new TextDecoder().decode(bytes)}]\n }\n if (status === 204) {\n await sleep(pollIntervalMs)\n continue\n }\n throw new SmartcatError(status, new TextDecoder().decode(bytes))\n }\n throw new Error(`Smartcat batch export of ${documentIds.length} document(s) timed out`)\n }\n\n private async request<T = unknown>(method: string, path: string, body?: BodyInit): Promise<T> {\n const {status, text} = await this.requestRaw(method, path, body)\n if (status < 200 || status >= 300) throw new SmartcatError(status, text)\n return (text ? JSON.parse(text) : null) as T\n }\n\n /**\n * Normalizes a `fetch` rejection. A timeout abort becomes a\n * {@link SmartcatTimeoutError} — traced through the request logger first (with\n * a synthetic status 0) so it surfaces in the same log as HTTP failures — while\n * anything else is rethrown unchanged.\n */\n private onFetchError(method: string, path: string, err: unknown): unknown {\n if (isAbortError(err) && this.requestTimeoutMs) {\n const timeout = new SmartcatTimeoutError(method, path, this.requestTimeoutMs)\n this.requestLogger?.({method, path, status: 0, body: timeout.message})\n return timeout\n }\n return err\n }\n\n private async requestRaw(\n method: string,\n path: string,\n body?: BodyInit,\n ): Promise<{status: number; text: string}> {\n let res: Response\n try {\n res = await this.fetchImpl(`https://${this.server}${path}`, {\n method,\n headers: {Authorization: this.authHeader},\n body,\n signal: this.timeoutSignal(),\n })\n } catch (err) {\n throw this.onFetchError(method, path, err)\n }\n const text = await res.text()\n this.requestLogger?.({method, path, status: res.status, body: text})\n return {status: res.status, text}\n }\n\n /** Like {@link requestRaw} but preserves the body as bytes (ZIP downloads). */\n private async requestBytes(method: string, path: string): Promise<{status: number; bytes: Uint8Array}> {\n let res: Response\n try {\n res = await this.fetchImpl(`https://${this.server}${path}`, {\n method,\n headers: {Authorization: this.authHeader},\n signal: this.timeoutSignal(),\n })\n } catch (err) {\n throw this.onFetchError(method, path, err)\n }\n const bytes = new Uint8Array(await res.arrayBuffer())\n // Log a size placeholder for successful binary payloads; error bodies are\n // text (Smartcat's error detail) and stay readable in the trace.\n const logBody = res.status >= 200 && res.status < 300 ? `<${bytes.length} bytes>` : new TextDecoder().decode(bytes)\n this.requestLogger?.({method, path, status: res.status, body: logBody})\n return {status: res.status, bytes}\n }\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n"],"names":[],"mappings":"AAoCO,SAAS,MAAM,OAA4B;AAChD,SACE,MAAM,UAAU,KAChB,MAAM,CAAC,MAAM,MACb,MAAM,CAAC,MAAM,MACb,MAAM,CAAC,MAAM,KACb,MAAM,CAAC,MAAM;AAEjB;AAGA,eAAsB,MAAM,OAAwC;AAClE,QAAM,OAAO,IAAI,SAAS,MAAM,QAAQ,MAAM,YAAY,MAAM,UAAU,GACpE,OAAO,0BAA0B,IAAI,GACrC,UAAsB,CAAA;AAE5B,MAAI,SAAS,KAAK;AAClB,WAAS,IAAI,GAAG,IAAI,KAAK,YAAY,KAAK;AACxC,QAAI,KAAK,UAAU,QAAQ,EAAI,MAAM;AACnC,YAAM,IAAI,MAAM,+CAA+C,MAAM,EAAE;AAEzE,UAAM,SAAS,KAAK,UAAU,SAAS,IAAI,EAAI;AAC/C,QAAI,iBAAyB,KAAK,UAAU,SAAS,IAAI,EAAI,GACzD,mBAA2B,KAAK,UAAU,SAAS,IAAI,EAAI;AAC/D,UAAM,aAAa,KAAK,UAAU,SAAS,IAAI,EAAI,GAC7C,cAAc,KAAK,UAAU,SAAS,IAAI,EAAI,GAC9C,gBAAgB,KAAK,UAAU,SAAS,IAAI,EAAI;AACtD,QAAI,oBAA4B,KAAK,UAAU,SAAS,IAAI,EAAI;AAChE,UAAM,OAAO,IAAI,YAAA,EAAc,OAAO,MAAM,SAAS,SAAS,IAAI,SAAS,KAAK,UAAU,CAAC;AAM3F,QAAI,mBAAmB,cAAW,qBAAqB,cAAW,sBAAsB,YAAS;AAC/F,YAAM,aAAa,SAAS,KAAK,YAC3B,WAAW,aAAa;AAC9B,UAAI,IAAI;AACR,aAAO,IAAI,KAAK,YAAU;AACxB,cAAM,KAAK,KAAK,UAAU,GAAG,EAAI,GAC3B,OAAO,KAAK,UAAU,IAAI,GAAG,EAAI;AACvC,YAAI,OAAO,GAAgB;AACzB,cAAI,IAAI,IAAI;AACR,+BAAqB,eACvB,mBAAmB,OAAO,KAAK,aAAa,GAAG,EAAI,CAAC,GACpD,KAAK,IAEH,mBAAmB,eACrB,iBAAiB,OAAO,KAAK,aAAa,GAAG,EAAI,CAAC,GAClD,KAAK,IAEH,sBAAsB,eACxB,oBAAoB,OAAO,KAAK,aAAa,GAAG,EAAI,CAAC;AAEvD;AAAA,QACF;AACA,aAAK,IAAI;AAAA,MACX;AACA,UAAI,mBAAmB,cAAW,sBAAsB;AACtD,cAAM,IAAI,MAAM,iBAAiB,IAAI,kDAAkD;AAAA,IAE3F;AAGA,QAFA,UAAU,KAAK,aAAa,cAAc,eAEtC,KAAK,SAAS,GAAG,EAAG;AAKxB,QAAI,KAAK,UAAU,mBAAmB,EAAI,MAAM;AAC9C,YAAM,IAAI,MAAM,sCAAsC,IAAI,GAAG;AAE/D,UAAM,kBAAkB,KAAK,UAAU,oBAAoB,IAAI,EAAI,GAC7D,mBAAmB,KAAK,UAAU,oBAAoB,IAAI,EAAI,GAC9D,YAAY,oBAAoB,KAAK,kBAAkB,kBACvD,aAAa,MAAM,SAAS,WAAW,YAAY,cAAc;AAEvE,YAAQ,KAAK,EAAC,MAAM,MAAM,MAAM,WAAW,QAAQ,YAAY,IAAI,GAAE;AAAA,EACvE;AACA,SAAO;AACT;AAGA,eAAsB,UAAU,OAA+D;AAC7F,QAAM,UAAU,MAAM,MAAM,KAAK,GAC3B,UAAU,IAAI,YAAA;AACpB,SAAO,QAAQ,IAAI,CAAC,OAAO,EAAC,MAAM,EAAE,MAAM,SAAS,QAAQ,OAAO,EAAE,IAAI,IAAG;AAC7E;AAEA,SAAS,0BAA0B,MAGjC;AACA,QAAM,SAAS,KAAK,IAAI,GAAG,KAAK,aAAa,KAAgB,KAAM;AACnE,WAAS,IAAI,KAAK,aAAa,IAAe,KAAK,QAAQ,KAAK;AAC9D,QAAI,KAAK,UAAU,GAAG,EAAI,MAAM,UAAU;AAE1C,QAAI,aAAqB,KAAK,UAAU,IAAI,IAAI,EAAI,GAChD,yBAAiC,KAAK,UAAU,IAAI,IAAI,EAAI;AAIhE,QAAI,eAAe,SAAW,2BAA2B,YAAS;AAChE,YAAM,UAAU,IAAI;AACpB,UAAI,UAAU,KAAK,KAAK,UAAU,SAAS,EAAI,MAAM;AACnD,cAAM,IAAI,MAAM,4EAA4E;AAE9F,YAAM,YAAY,OAAO,KAAK,aAAa,UAAU,GAAG,EAAI,CAAC;AAC7D,UAAI,KAAK,UAAU,WAAW,EAAI,MAAM;AACtC,cAAM,IAAI,MAAM,wDAAwD;AAE1E,mBAAa,OAAO,KAAK,aAAa,YAAY,IAAI,EAAI,CAAC,GAC3D,yBAAyB,OAAO,KAAK,aAAa,YAAY,IAAI,EAAI,CAAC;AAAA,IACzE;AACA,WAAO,EAAC,YAAY,uBAAA;AAAA,EACtB;AACA,QAAM,IAAI,MAAM,wDAAwD;AAC1E;AAEA,eAAe,WAAW,QAAgB,MAAkB,MAAmC;AAC7F,MAAI,WAAW,EAAG,QAAO;AACzB,MAAI,WAAW,GAAG;AAEhB,UAAM,SAAS,IAAI,KAAK,CAAC,KAAK,MAAA,CAAO,CAAC,EAAE,SAAS,YAAY,IAAI,oBAAoB,aAAa,CAAC;AACnG,WAAO,IAAI,WAAW,MAAM,IAAI,SAAS,MAAM,EAAE,aAAa;AAAA,EAChE;AACA,QAAM,IAAI,MAAM,sCAAsC,MAAM,SAAS,IAAI,GAAG;AAC9E;ACzJA,MAAM,WAAW;AAUV,MAAM,sBAAsB,MAAM;AAAA,EACvC,YACkB,QACA,MAChB;AACA,UAAM,sBAAsB,MAAM,KAAK,IAAI,EAAE,GAH7B,KAAA,SAAA,QACA,KAAA,OAAA,MAGhB,KAAK,OAAO;AAAA,EACd;AACF;AAGO,MAAM,6BAA6B,MAAM;AAAA,EAC9C,YACkB,QACA,MACA,WAChB;AACA,UAAM,mCAAmC,eAAe,SAAS,CAAC,QAAQ,MAAM,IAAI,IAAI,EAAE,GAJ1E,KAAA,SAAA,QACA,KAAA,OAAA,MACA,KAAA,YAAA,WAGhB,KAAK,OAAO;AAAA,EACd;AACF;AAGA,SAAS,eAAe,IAAoB;AAC1C,QAAM,eAAe,KAAK,MAAM,KAAK,GAAI,GACnC,UAAU,KAAK,MAAM,eAAe,EAAE,GACtC,UAAU,eAAe;AAC/B,SAAK,UACE,UAAU,GAAG,OAAO,IAAI,OAAO,MAAM,GAAG,OAAO,MADjC,GAAG,OAAO;AAEjC;AAGA,SAAS,aAAa,KAAuB;AAC3C,QAAM,OAAQ,KAAqC;AACnD,SAAO,SAAS,kBAAkB,SAAS;AAC7C;AASO,MAAM,eAAe;AAAA,EAO1B,YAAY,SAAgC;AAC1C,SAAK,SAAS,QAAQ,OAAO,QAAQ,gBAAgB,EAAE,EAAE,QAAQ,OAAO,EAAE,GAC1E,KAAK,aAAa,SAAS,KAAK,GAAG,QAAQ,WAAW,IAAI,QAAQ,MAAM,EAAE,CAAC,IAC3E,KAAK,YAAY,QAAQ,aAAa,OACtC,KAAK,mBAAmB,QAAQ;AAAA,EAClC;AAAA;AAAA,EAGQ,gBAAyC;AAC/C,WAAO,KAAK,mBAAmB,YAAY,QAAQ,KAAK,gBAAgB,IAAI;AAAA,EAC9E;AAAA;AAAA,EAGA,iBAAiB,QAA6B;AAC5C,SAAK,gBAAgB;AAAA,EACvB;AAAA;AAAA,EAGA,MAAM,cAAc,OAAqD;AACvE,UAAM,EAAC,YAAY,GAAG,KAAA,IAAQ,OAKxB,OAAO,aACT,EAAC,gBAAgB,IAAO,aAAa,0BAA0B,YAAY,GAAG,SAC9E;AAAA,MACE,gBAAgB;AAAA,MAChB,aAAa;AAAA,MACb,OAAO;AAAA,MACP,sBAAsB;AAAA,MACtB,cAAc;AAAA,MACd,0BAA0B;AAAA,MAC1B,8BAA8B;AAAA,MAC9B,GAAG;AAAA,IAAA,GAEH,OAAO,IAAI,SAAA;AACjB,WAAA,KAAK,OAAO,QAAQ,KAAK,UAAU,IAAI,CAAC,GACjC,KAAK,QAAQ,QAAQ,GAAG,QAAQ,mBAAmB,IAAI;AAAA,EAChE;AAAA;AAAA,EAGA,MAAM,eAAmD;AACvD,WAAO,KAAK,QAAQ,OAAO,GAAG,QAAQ,WAAW;AAAA,EACnD;AAAA;AAAA,EAGA,MAAM,eACJ,WACA,UACA,SAC6B;AAC7B,UAAM,OAAO,IAAI,SAAA;AAGjB,SAAK,OAAO,QAAQ,IAAI,KAAK,CAAC,OAAO,GAAG,EAAC,MAAM,2BAAA,CAA2B,GAAG,QAAQ;AACrF,UAAM,SAAS,MAAM,KAAK;AAAA,MACxB;AAAA,MACA,GAAG,QAAQ,+BAA+B,mBAAmB,SAAS,CAAC;AAAA,MACvE;AAAA,IAAA;AAEF,WAAO,MAAM,QAAQ,MAAM,IAAI,SAAS,SAAS,CAAC,MAAM,IAAI,CAAA;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,gBACJ,WACA,OAC6B;AAC7B,UAAM,OAAO,IAAI,SAAA;AAGjB,SAAK,OAAO,SAAS,IAAI,KAAK,CAAC,KAAK,UAAU,MAAM,IAAI,OAAO,CAAA,EAAG,CAAC,CAAC,GAAG,EAAC,MAAM,mBAAA,CAAmB,GAAG,OAAO;AAC3G,eAAW,KAAK;AACd,WAAK,OAAO,QAAQ,IAAI,KAAK,CAAC,EAAE,OAAO,GAAG,EAAC,MAAM,2BAAA,CAA2B,GAAG,EAAE,QAAQ;AAE3F,UAAM,SAAS,MAAM,KAAK;AAAA,MACxB;AAAA,MACA,GAAG,QAAQ,+BAA+B,mBAAmB,SAAS,CAAC;AAAA,MACvE;AAAA,IAAA;AAEF,WAAO,MAAM,QAAQ,MAAM,IAAI,SAAS,SAAS,CAAC,MAAM,IAAI,CAAA;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,eACJ,YACA,UACA,SAC6B;AAC7B,UAAM,OAAO,IAAI,SAAA;AACjB,SAAK,OAAO,QAAQ,IAAI,KAAK,CAAC,OAAO,GAAG,EAAC,MAAM,2BAAA,CAA2B,GAAG,QAAQ;AACrF,UAAM,SAAS,MAAM,KAAK;AAAA,MACxB;AAAA,MACA,GAAG,QAAQ,+BAA+B,mBAAmB,UAAU,CAAC;AAAA,MACxE;AAAA,IAAA;AAEF,WAAO,MAAM,QAAQ,MAAM,IAAI,SAAS,SAAS,CAAC,MAAM,IAAI,CAAA;AAAA,EAC9D;AAAA;AAAA,EAGA,MAAM,WAAW,WAA6C;AAC5D,WAAO,KAAK,QAAQ,OAAO,GAAG,QAAQ,YAAY,mBAAmB,SAAS,CAAC,EAAE;AAAA,EACnF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,eAAe,YAAmC;AACtD,UAAM,KAAK,QAAQ,UAAU,GAAG,QAAQ,yBAAyB,mBAAmB,UAAU,CAAC,EAAE;AAAA,EACnG;AAAA;AAAA,EAGA,WAAW,WAA2B;AACpC,WAAO,WAAW,KAAK,MAAM,aAAa,SAAS;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,eACJ,YACA,UAKI,IACa;AACjB,UAAM,EAAC,OAAO,aAAa,iBAAiB,KAAM,cAAc,IAAI,QAAQ,aAAA,IAAgB,SAEtF,OAAO,MAAM,KAAK;AAAA,MACtB;AAAA,MACA,GAAG,QAAQ,gCAAgC,mBAAmB,UAAU,CAAC,SAAS,IAAI;AAAA,IAAA;AAGxF,aAAS,UAAU,GAAG,UAAU,aAAa,WAAW;AACtD,YAAM,EAAC,QAAQ,SAAQ,MAAM,KAAK,WAAW,OAAO,GAAG,QAAQ,oBAAoB,KAAK,EAAE,EAAE;AAC5F,UAAI,WAAW,IAAK,QAAO;AAC3B,UAAI,WAAW,KAAK;AAClB,cAAM,MAAM,cAAc;AAC1B;AAAA,MACF;AACA,YAAM,IAAI,cAAc,QAAQ,IAAI;AAAA,IACtC;AACA,UAAM,IAAI,MAAM,uBAAuB,UAAU,YAAY;AAAA,EAC/D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,qBACJ,aACA,UAKI,IACqB;AACzB,QAAI,YAAY,WAAW,EAAG,QAAO,CAAA;AAErC,UAAM,EAAC,OAAO,aAAa,iBAAiB,KAAM,cAAc,IAAI,QAAQ,aAAA,IAAgB,SAEtF,QAAQ,YAAY,IAAI,CAAC,OAAO,eAAe,mBAAmB,EAAE,CAAC,EAAE,EAAE,KAAK,GAAG,GACjF,OAAO,MAAM,KAAK;AAAA,MACtB;AAAA,MACA,GAAG,QAAQ,oBAAoB,KAAK,SAAS,IAAI;AAAA,IAAA;AAGnD,aAAS,UAAU,GAAG,UAAU,aAAa,WAAW;AACtD,YAAM,EAAC,QAAQ,UAAS,MAAM,KAAK,aAAa,OAAO,GAAG,QAAQ,oBAAoB,KAAK,EAAE,EAAE;AAC/F,UAAI,WAAW;AAIb,eAAI,MAAM,KAAK,KACL,MAAM,UAAU,KAAK,GAAG,IAAI,CAAC,EAAC,MAAM,QAAA,OAAc,EAAC,UAAU,MAAM,QAAA,EAAS,IAE/E,CAAC,EAAC,UAAU,IAAI,SAAS,IAAI,YAAA,EAAc,OAAO,KAAK,GAAE;AAElE,UAAI,WAAW,KAAK;AAClB,cAAM,MAAM,cAAc;AAC1B;AAAA,MACF;AACA,YAAM,IAAI,cAAc,QAAQ,IAAI,cAAc,OAAO,KAAK,CAAC;AAAA,IACjE;AACA,UAAM,IAAI,MAAM,4BAA4B,YAAY,MAAM,wBAAwB;AAAA,EACxF;AAAA,EAEA,MAAc,QAAqB,QAAgB,MAAc,MAA6B;AAC5F,UAAM,EAAC,QAAQ,SAAQ,MAAM,KAAK,WAAW,QAAQ,MAAM,IAAI;AAC/D,QAAI,SAAS,OAAO,UAAU,WAAW,IAAI,cAAc,QAAQ,IAAI;AACvE,WAAQ,OAAO,KAAK,MAAM,IAAI,IAAI;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,aAAa,QAAgB,MAAc,KAAuB;AACxE,QAAI,aAAa,GAAG,KAAK,KAAK,kBAAkB;AAC9C,YAAM,UAAU,IAAI,qBAAqB,QAAQ,MAAM,KAAK,gBAAgB;AAC5E,aAAA,KAAK,gBAAgB,EAAC,QAAQ,MAAM,QAAQ,GAAG,MAAM,QAAQ,QAAA,CAAQ,GAC9D;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,WACZ,QACA,MACA,MACyC;AACzC,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,KAAK,UAAU,WAAW,KAAK,MAAM,GAAG,IAAI,IAAI;AAAA,QAC1D;AAAA,QACA,SAAS,EAAC,eAAe,KAAK,WAAA;AAAA,QAC9B;AAAA,QACA,QAAQ,KAAK,cAAA;AAAA,MAAc,CAC5B;AAAA,IACH,SAAS,KAAK;AACZ,YAAM,KAAK,aAAa,QAAQ,MAAM,GAAG;AAAA,IAC3C;AACA,UAAM,OAAO,MAAM,IAAI,KAAA;AACvB,WAAA,KAAK,gBAAgB,EAAC,QAAQ,MAAM,QAAQ,IAAI,QAAQ,MAAM,KAAA,CAAK,GAC5D,EAAC,QAAQ,IAAI,QAAQ,KAAA;AAAA,EAC9B;AAAA;AAAA,EAGA,MAAc,aAAa,QAAgB,MAA4D;AACrG,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,KAAK,UAAU,WAAW,KAAK,MAAM,GAAG,IAAI,IAAI;AAAA,QAC1D;AAAA,QACA,SAAS,EAAC,eAAe,KAAK,WAAA;AAAA,QAC9B,QAAQ,KAAK,cAAA;AAAA,MAAc,CAC5B;AAAA,IACH,SAAS,KAAK;AACZ,YAAM,KAAK,aAAa,QAAQ,MAAM,GAAG;AAAA,IAC3C;AACA,UAAM,QAAQ,IAAI,WAAW,MAAM,IAAI,aAAa,GAG9C,UAAU,IAAI,UAAU,OAAO,IAAI,SAAS,MAAM,IAAI,MAAM,MAAM,YAAY,IAAI,YAAA,EAAc,OAAO,KAAK;AAClH,WAAA,KAAK,gBAAgB,EAAC,QAAQ,MAAM,QAAQ,IAAI,QAAQ,MAAM,QAAA,CAAQ,GAC/D,EAAC,QAAQ,IAAI,QAAQ,MAAA;AAAA,EAC9B;AACF;AAEA,SAAS,aAAa,IAA2B;AAC/C,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;"}
1
+ {"version":3,"file":"smartcat.js","sources":["../src/lib/zip.ts","../src/smartcat/client.ts"],"sourcesContent":["/**\n * Minimal ZIP reader for Smartcat batch-export archives.\n *\n * Dependency-free by design (like the rest of the Function-side code): built on\n * `DataView` + the global `DecompressionStream` (Node 18+ / browsers). Supports\n * the two compression methods Smartcat archives use — stored (0) and deflate (8).\n *\n * ZIP64 is REQUIRED, not an edge case: Smartcat writes export archives with\n * SharpZipLib's `UseZip64 = On` (forced), so central-directory size/offset\n * fields hold `0xFFFFFFFF` placeholders with the real 64-bit values in each\n * entry's ZIP64 extra field — even for tiny archives. Large archives\n * additionally use the ZIP64 end-of-central-directory record.\n */\n\nexport interface ZipEntry {\n /** Entry path inside the archive (folders separated by `/`). */\n name: string\n data: Uint8Array\n}\n\nconst LOCAL_HEADER_SIG = 0x04034b50\nconst CENTRAL_HEADER_SIG = 0x02014b50\nconst EOCD_SIG = 0x06054b50\nconst ZIP64_EOCD_SIG = 0x06064b50\nconst ZIP64_EOCD_LOCATOR_SIG = 0x07064b50\n/** EOCD record is 22 bytes + an optional comment of up to 65535 bytes. */\nconst EOCD_MIN_SIZE = 22\n/** ZIP64 EOCD locator is a fixed 20 bytes, sitting right before the classic EOCD. */\nconst ZIP64_LOCATOR_SIZE = 20\n/** Placeholder meaning \"the real value is in the ZIP64 extra field / record\". */\nconst U32_MAX = 0xffffffff\nconst U16_MAX = 0xffff\n/** Extra-field header id of the ZIP64 extended information block. */\nconst ZIP64_EXTRA_ID = 0x0001\n\n/** Whether the payload looks like a ZIP archive (`PK\\x03\\x04` magic). */\nexport function isZip(bytes: Uint8Array): boolean {\n return (\n bytes.length >= 4 &&\n bytes[0] === 0x50 &&\n bytes[1] === 0x4b &&\n bytes[2] === 0x03 &&\n bytes[3] === 0x04\n )\n}\n\n/** Extracts all file entries (directories are skipped). */\nexport async function unzip(bytes: Uint8Array): Promise<ZipEntry[]> {\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n const eocd = findEndOfCentralDirectory(view)\n const entries: ZipEntry[] = []\n\n let offset = eocd.centralDirectoryOffset\n for (let i = 0; i < eocd.entryCount; i++) {\n if (view.getUint32(offset, true) !== CENTRAL_HEADER_SIG) {\n throw new Error(`Invalid ZIP: bad central directory entry at ${offset}`)\n }\n const method = view.getUint16(offset + 10, true)\n let compressedSize: number = view.getUint32(offset + 20, true)\n let uncompressedSize: number = view.getUint32(offset + 24, true)\n const nameLength = view.getUint16(offset + 28, true)\n const extraLength = view.getUint16(offset + 30, true)\n const commentLength = view.getUint16(offset + 32, true)\n let localHeaderOffset: number = view.getUint32(offset + 42, true)\n const name = new TextDecoder().decode(bytes.subarray(offset + 46, offset + 46 + nameLength))\n\n // ZIP64: any field at its 32-bit max is a placeholder; the real value lives\n // in the entry's ZIP64 extra block, in a fixed order, holding ONLY the\n // fields that overflowed (Smartcat's forced-ZIP64 writer always defers the\n // sizes this way, even for small files).\n if (compressedSize === U32_MAX || uncompressedSize === U32_MAX || localHeaderOffset === U32_MAX) {\n const extraStart = offset + 46 + nameLength\n const extraEnd = extraStart + extraLength\n let p = extraStart\n while (p + 4 <= extraEnd) {\n const id = view.getUint16(p, true)\n const size = view.getUint16(p + 2, true)\n if (id === ZIP64_EXTRA_ID) {\n let q = p + 4\n if (uncompressedSize === U32_MAX) {\n uncompressedSize = Number(view.getBigUint64(q, true))\n q += 8\n }\n if (compressedSize === U32_MAX) {\n compressedSize = Number(view.getBigUint64(q, true))\n q += 8\n }\n if (localHeaderOffset === U32_MAX) {\n localHeaderOffset = Number(view.getBigUint64(q, true))\n }\n break\n }\n p += 4 + size\n }\n if (compressedSize === U32_MAX || localHeaderOffset === U32_MAX) {\n throw new Error(`Invalid ZIP: \"${name}\" needs ZIP64 sizes but has no ZIP64 extra field`)\n }\n }\n offset += 46 + nameLength + extraLength + commentLength\n\n if (name.endsWith('/')) continue // directory entry\n\n // The local header repeats name/extra with its own lengths; the data\n // follows it. Sizes come from the central directory (authoritative even\n // when the local header deferred them to a data descriptor).\n if (view.getUint32(localHeaderOffset, true) !== LOCAL_HEADER_SIG) {\n throw new Error(`Invalid ZIP: bad local header for \"${name}\"`)\n }\n const localNameLength = view.getUint16(localHeaderOffset + 26, true)\n const localExtraLength = view.getUint16(localHeaderOffset + 28, true)\n const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength\n const compressed = bytes.subarray(dataStart, dataStart + compressedSize)\n\n entries.push({name, data: await decompress(method, compressed, name)})\n }\n return entries\n}\n\n/** Convenience: unzip and decode every file entry as UTF-8 text. */\nexport async function unzipText(bytes: Uint8Array): Promise<{name: string; content: string}[]> {\n const entries = await unzip(bytes)\n const decoder = new TextDecoder()\n return entries.map((e) => ({name: e.name, content: decoder.decode(e.data)}))\n}\n\nfunction findEndOfCentralDirectory(view: DataView): {\n entryCount: number\n centralDirectoryOffset: number\n} {\n const lowest = Math.max(0, view.byteLength - EOCD_MIN_SIZE - 0xffff)\n for (let i = view.byteLength - EOCD_MIN_SIZE; i >= lowest; i--) {\n if (view.getUint32(i, true) !== EOCD_SIG) continue\n\n let entryCount: number = view.getUint16(i + 10, true)\n let centralDirectoryOffset: number = view.getUint32(i + 16, true)\n\n // ZIP64: placeholder values defer to the ZIP64 EOCD record, found via the\n // fixed-size locator that sits immediately before the classic EOCD.\n if (entryCount === U16_MAX || centralDirectoryOffset === U32_MAX) {\n const locator = i - ZIP64_LOCATOR_SIZE\n if (locator < 0 || view.getUint32(locator, true) !== ZIP64_EOCD_LOCATOR_SIG) {\n throw new Error('Invalid ZIP: ZIP64 values without a ZIP64 end-of-central-directory locator')\n }\n const zip64Eocd = Number(view.getBigUint64(locator + 8, true))\n if (view.getUint32(zip64Eocd, true) !== ZIP64_EOCD_SIG) {\n throw new Error('Invalid ZIP: bad ZIP64 end-of-central-directory record')\n }\n entryCount = Number(view.getBigUint64(zip64Eocd + 32, true))\n centralDirectoryOffset = Number(view.getBigUint64(zip64Eocd + 48, true))\n }\n return {entryCount, centralDirectoryOffset}\n }\n throw new Error('Invalid ZIP: end-of-central-directory record not found')\n}\n\nasync function decompress(method: number, data: Uint8Array, name: string): Promise<Uint8Array> {\n if (method === 0) return data // stored\n if (method === 8) {\n // Copy into a fresh ArrayBuffer so the Blob sees exactly this entry's bytes.\n const stream = new Blob([data.slice()]).stream().pipeThrough(new DecompressionStream('deflate-raw'))\n return new Uint8Array(await new Response(stream).arrayBuffer())\n }\n throw new Error(`Unsupported ZIP compression method ${method} for \"${name}\"`)\n}\n","import {isZip, unzipText} from '../lib/zip'\nimport type {\n CreateProjectInput,\n RequestLogger,\n SmartcatClientOptions,\n SmartcatDocument,\n SmartcatProject,\n SmartcatTemplatesResponse,\n} from './types'\n\nconst API_BASE = '/api/integration/v1'\n\n/** One file from a (batch) export: archive entry name + LocJSON text. */\nexport interface ExportedFile {\n /** Path inside the export archive; empty for a single-file response. */\n filename: string\n content: string\n}\n\n/** Error thrown when the Smartcat API returns a non-2xx response. */\nexport class SmartcatError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: string,\n ) {\n super(`Smartcat API error ${status}: ${body}`)\n this.name = 'SmartcatError'\n }\n}\n\n/** Error thrown when a request is aborted by the configured `requestTimeoutMs`. */\nexport class SmartcatTimeoutError extends Error {\n constructor(\n public readonly method: string,\n public readonly path: string,\n public readonly timeoutMs: number,\n ) {\n super(`Smartcat did not respond within ${formatDuration(timeoutMs)} for ${method} ${path}`)\n this.name = 'SmartcatTimeoutError'\n }\n}\n\n/** Human-readable duration, e.g. 570000 → \"9m30s\", 30000 → \"30s\". */\nfunction formatDuration(ms: number): string {\n const totalSeconds = Math.round(ms / 1000)\n const minutes = Math.floor(totalSeconds / 60)\n const seconds = totalSeconds % 60\n if (!minutes) return `${seconds}s`\n return seconds ? `${minutes}m${seconds}s` : `${minutes}m`\n}\n\n/** True for an abort/timeout rejection from `fetch` (DOMException-style). */\nfunction isAbortError(err: unknown): boolean {\n const name = (err as {name?: string} | undefined)?.name\n return name === 'TimeoutError' || name === 'AbortError'\n}\n\n/**\n * Minimal client for the Smartcat integration API.\n *\n * Uses Basic auth (`base64(workspaceId:apiKey)`). Built on global `fetch`,\n * `FormData` and `Blob` (Node 18+ / the Functions runtime); no Node-only APIs,\n * so it is portable. Secrets are required, so this only runs server-side.\n */\nexport class SmartcatClient {\n private readonly server: string\n private readonly authHeader: string\n private readonly fetchImpl: typeof fetch\n private readonly requestTimeoutMs?: number\n private requestLogger?: RequestLogger\n\n constructor(options: SmartcatClientOptions) {\n this.server = options.server.replace(/^https?:\\/\\//, '').replace(/\\/$/, '')\n this.authHeader = `Basic ${btoa(`${options.workspaceId}:${options.apiKey}`)}`\n this.fetchImpl = options.fetchImpl ?? fetch\n this.requestTimeoutMs = options.requestTimeoutMs\n }\n\n /** An `AbortSignal` that fires after `requestTimeoutMs`, or undefined if unset. */\n private timeoutSignal(): AbortSignal | undefined {\n return this.requestTimeoutMs ? AbortSignal.timeout(this.requestTimeoutMs) : undefined\n }\n\n /** Attach a sink that receives every HTTP call (method, path, status, body). */\n setRequestLogger(logger: RequestLogger): void {\n this.requestLogger = logger\n }\n\n /** Creates a project and returns it (including its generated `id` GUID). */\n async createProject(input: CreateProjectInput): Promise<SmartcatProject> {\n const {templateId, ...rest} = input\n // A template fully owns its workflow, MT/TM and pretranslation config, so we\n // send only the project identity + template id. The standard flow sets the\n // MT/TM/pretranslation flags here; `workflowStages` is supplied by the caller\n // (it's what distinguishes AI+human from AI-only).\n const body = templateId\n ? {assignToVendor: false, externalTag: 'smartcat-sanity-plugin', templateId, ...rest}\n : {\n assignToVendor: false,\n externalTag: 'smartcat-sanity-plugin',\n useMT: true,\n useTranslationMemory: true,\n pretranslate: true,\n autoPropagateRepetitions: true,\n minTranslationAssuranceLevel: 'regular',\n ...rest,\n }\n const form = new FormData()\n form.append('body', JSON.stringify(body))\n return this.request('POST', `${API_BASE}/project/create`, form)\n }\n\n /** Lists the workspace's project templates. */\n async getTemplates(): Promise<SmartcatTemplatesResponse> {\n return this.request('GET', `${API_BASE}/template`)\n }\n\n /**\n * Uploads one source file into a project. `filename` may include a folder path.\n *\n * When `targetLanguage` is given (a Smartcat language code), the file is\n * restricted to that single target language via the `documentModel` settings\n * part — used by the \"send existing translations\" flow, which uploads one\n * bilingual LocJSON per language. Omitted ⇒ the document targets every project\n * language (today's source-only default).\n */\n async uploadDocument(\n projectId: string,\n filename: string,\n content: string,\n targetLanguage?: string,\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n if (targetLanguage) {\n // `documentModel` is a JSON array (one entry per file); a bare object 400s.\n const model = [{targetLanguages: [targetLanguage]}]\n form.append('documentModel', new Blob([JSON.stringify(model)], {type: 'application/json'}), 'documentModel')\n }\n // Use a generic content type: declaring application/json makes Smartcat try\n // to map the file to an API model instead of importing it as a document.\n form.append('file', new Blob([content], {type: 'application/octet-stream'}), filename)\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'POST',\n `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /**\n * Uploads many source files into a project in a single request — the\n * multi-file sibling of {@link uploadDocument}. Mirrors the reference\n * `AddDocuments` multipart contract: one `model` part carrying a JSON array\n * with one (default-settings) entry per file, plus one `file` part per file.\n * Returns the created documents (files × target languages).\n */\n async uploadDocuments(\n projectId: string,\n files: {filename: string; content: string}[],\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n // One settings object per file, positional with the file parts (empty = default\n // disassembly, as the single-file path). Correlation never relies on this order.\n form.append('model', new Blob([JSON.stringify(files.map(() => ({})))], {type: 'application/json'}), 'model')\n for (const f of files) {\n form.append('file', new Blob([f.content], {type: 'application/octet-stream'}), f.filename)\n }\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'POST',\n `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /**\n * Updates an existing document's source content by re-uploading the file.\n * Smartcat re-imports and merges, preserving translations of unchanged\n * segments. `documentId` is a target document id (`<fileGuid>_<langNum>`).\n */\n async updateDocument(\n documentId: string,\n filename: string,\n content: string,\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n form.append('file', new Blob([content], {type: 'application/octet-stream'}), filename)\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'PUT',\n `${API_BASE}/document/update?documentId=${encodeURIComponent(documentId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /** Fetches a project (including its documents and per-stage progress). */\n async getProject(projectId: string): Promise<SmartcatProject> {\n return this.request('GET', `${API_BASE}/project/${encodeURIComponent(projectId)}`)\n }\n\n /**\n * Deletes a document from its project. The id is a target document id\n * (`<fileGuid>_<langNum>`), but Smartcat removes the whole document — every\n * target language — in a single call.\n */\n async deleteDocument(documentId: string): Promise<void> {\n await this.request('DELETE', `${API_BASE}/document?documentIds=${encodeURIComponent(documentId)}`)\n }\n\n /** Builds the Smartcat web URL for a project's overview page. */\n projectUrl(projectId: string): string {\n return `https://${this.server}/projects/${projectId}/general`\n }\n\n /**\n * Exports a document's target (translated) content and returns it as text.\n * Two-step: request an export task, then poll until ready.\n *\n * `mode` defaults to `confirmed` — only segments confirmed at any workflow\n * stage are returned (unconfirmed/untranslated segments fall back to source),\n * so we never import unvetted machine-translation drafts. Use `current` to pull\n * the live state including unconfirmed edits.\n */\n async exportDocument(\n documentId: string,\n options: {\n mode?: 'confirmed' | 'current'\n pollIntervalMs?: number\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n } = {},\n ): Promise<string> {\n const {mode = 'confirmed', pollIntervalMs = 3000, maxAttempts = 20, sleep = defaultSleep} = options\n\n const task = await this.request<{id: string}>(\n 'POST',\n `${API_BASE}/document/export?documentIds=${encodeURIComponent(documentId)}&mode=${mode}&type=target`,\n )\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const {status, text} = await this.requestRaw('GET', `${API_BASE}/document/export/${task.id}`)\n if (status === 200) return text\n if (status === 204) {\n await sleep(pollIntervalMs)\n continue\n }\n throw new SmartcatError(status, text)\n }\n throw new Error(`Smartcat export for ${documentId} timed out`)\n }\n\n /**\n * Exports many documents' target content in **one** export task. Smartcat\n * fans the work out server-side and (for more than one document) returns a\n * ZIP archive of the individual files, so the per-document request+poll\n * round-trip is paid once per batch instead of once per document.\n *\n * Identity comes from each LocJSON's own `x-sanity` metadata, not the entry\n * name, so callers should correlate on the parsed content.\n */\n async exportDocumentsBatch(\n documentIds: string[],\n options: {\n mode?: 'confirmed' | 'current'\n pollIntervalMs?: number\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n } = {},\n ): Promise<ExportedFile[]> {\n if (documentIds.length === 0) return []\n // Bigger batches take longer to assemble server-side; poll longer, not faster.\n const {mode = 'confirmed', pollIntervalMs = 3000, maxAttempts = 60, sleep = defaultSleep} = options\n\n const query = documentIds.map((id) => `documentIds=${encodeURIComponent(id)}`).join('&')\n const task = await this.request<{id: string}>(\n 'POST',\n `${API_BASE}/document/export?${query}&mode=${mode}&type=target`,\n )\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const {status, bytes} = await this.requestBytes('GET', `${API_BASE}/document/export/${task.id}`)\n if (status === 200) {\n // A single-document task returns the bare file; multi-document tasks\n // return a ZIP. Decide by content, not by count — Smartcat may drop\n // documents it can't export from the archive.\n if (isZip(bytes)) {\n return (await unzipText(bytes)).map(({name, content}) => ({filename: name, content}))\n }\n return [{filename: '', content: new TextDecoder().decode(bytes)}]\n }\n if (status === 204) {\n await sleep(pollIntervalMs)\n continue\n }\n throw new SmartcatError(status, new TextDecoder().decode(bytes))\n }\n throw new Error(`Smartcat batch export of ${documentIds.length} document(s) timed out`)\n }\n\n private async request<T = unknown>(method: string, path: string, body?: BodyInit): Promise<T> {\n const {status, text} = await this.requestRaw(method, path, body)\n if (status < 200 || status >= 300) throw new SmartcatError(status, text)\n return (text ? JSON.parse(text) : null) as T\n }\n\n /**\n * Normalizes a `fetch` rejection. A timeout abort becomes a\n * {@link SmartcatTimeoutError} — traced through the request logger first (with\n * a synthetic status 0) so it surfaces in the same log as HTTP failures — while\n * anything else is rethrown unchanged.\n */\n private onFetchError(method: string, path: string, err: unknown): unknown {\n if (isAbortError(err) && this.requestTimeoutMs) {\n const timeout = new SmartcatTimeoutError(method, path, this.requestTimeoutMs)\n this.requestLogger?.({method, path, status: 0, body: timeout.message})\n return timeout\n }\n return err\n }\n\n private async requestRaw(\n method: string,\n path: string,\n body?: BodyInit,\n ): Promise<{status: number; text: string}> {\n let res: Response\n try {\n res = await this.fetchImpl(`https://${this.server}${path}`, {\n method,\n headers: {Authorization: this.authHeader},\n body,\n signal: this.timeoutSignal(),\n })\n } catch (err) {\n throw this.onFetchError(method, path, err)\n }\n const text = await res.text()\n this.requestLogger?.({method, path, status: res.status, body: text})\n return {status: res.status, text}\n }\n\n /** Like {@link requestRaw} but preserves the body as bytes (ZIP downloads). */\n private async requestBytes(method: string, path: string): Promise<{status: number; bytes: Uint8Array}> {\n let res: Response\n try {\n res = await this.fetchImpl(`https://${this.server}${path}`, {\n method,\n headers: {Authorization: this.authHeader},\n signal: this.timeoutSignal(),\n })\n } catch (err) {\n throw this.onFetchError(method, path, err)\n }\n const bytes = new Uint8Array(await res.arrayBuffer())\n // Log a size placeholder for successful binary payloads; error bodies are\n // text (Smartcat's error detail) and stay readable in the trace.\n const logBody = res.status >= 200 && res.status < 300 ? `<${bytes.length} bytes>` : new TextDecoder().decode(bytes)\n this.requestLogger?.({method, path, status: res.status, body: logBody})\n return {status: res.status, bytes}\n }\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartcat/sanity-plugin",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
@@ -56,6 +56,10 @@ export function createAddToProjectAction(config: ResolvedSmartcatConfig): Docume
56
56
  const [newName, setNewName] = useState('')
57
57
  const [busy, setBusy] = useState(false)
58
58
  const [includeLinked, setIncludeLinked] = useState(true)
59
+ // Whether to also send the item's existing translations (one bilingual file
60
+ // per target language) rather than only its source text. Off by default:
61
+ // imported translations land confirmed at every stage, skipping human review.
62
+ const [sendExisting, setSendExisting] = useState(false)
59
63
  const {templates, loaded, requestRefresh} = useTemplates()
60
64
  const [workflow, setWorkflow] = useWorkflowSelection(open, templates, loaded)
61
65
 
@@ -103,6 +107,7 @@ export function createAddToProjectAction(config: ResolvedSmartcatConfig): Docume
103
107
  docId,
104
108
  docType,
105
109
  sourceIsPublished,
110
+ sendExistingTranslations: sendExisting,
106
111
  })
107
112
 
108
113
  // The root document plus, optionally, everything it links to — de-duped by id.
@@ -159,7 +164,7 @@ export function createAddToProjectAction(config: ResolvedSmartcatConfig): Docume
159
164
  } finally {
160
165
  setBusy(false)
161
166
  }
162
- }, [selection, newName, workflow, publishedId, doc, includeLinked, sourceIsPublished, schema, client, projects, close])
167
+ }, [selection, newName, workflow, publishedId, doc, includeLinked, sendExisting, sourceIsPublished, schema, client, projects, close])
163
168
 
164
169
  const content = useMemo(() => {
165
170
  if (projects === null) {
@@ -239,6 +244,22 @@ export function createAddToProjectAction(config: ResolvedSmartcatConfig): Docume
239
244
  </Text>
240
245
  </Stack>
241
246
  </Flex>
247
+ <Flex align="flex-start" gap={3}>
248
+ <Switch
249
+ checked={sendExisting}
250
+ onChange={(e) => setSendExisting(e.currentTarget.checked)}
251
+ disabled={busy}
252
+ />
253
+ <Stack space={2}>
254
+ <Text size={1} weight="medium">
255
+ Send existing translations
256
+ </Text>
257
+ <Text size={1} muted>
258
+ Upload current translations alongside the source, so Smartcat starts pre-filled.
259
+ Imported translations are marked confirmed at every stage — they skip human review.
260
+ </Text>
261
+ </Stack>
262
+ </Flex>
242
263
  </Stack>
243
264
  </Card>
244
265
 
@@ -255,7 +276,7 @@ export function createAddToProjectAction(config: ResolvedSmartcatConfig): Docume
255
276
  </Flex>
256
277
  </Stack>
257
278
  )
258
- }, [projects, selection, newName, workflow, templates, busy, includeLinked, docTitle, sourceIsPublished, close, handleConfirm])
279
+ }, [projects, selection, newName, workflow, templates, busy, includeLinked, sendExisting, docTitle, sourceIsPublished, close, handleConfirm])
259
280
 
260
281
  return {
261
282
  label: 'Add to translation project',