@templatical/ai-kit 0.52.1 → 0.54.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 (33) hide show
  1. package/dist/adapters/anthropic.js +1 -1
  2. package/dist/adapters/openai.js +1 -1
  3. package/dist/adapters/vercel.js +1 -1
  4. package/dist/checks-BM_JnvRo.js +683 -0
  5. package/dist/checks-BM_JnvRo.js.map +1 -0
  6. package/dist/client/index.d.ts +8 -2
  7. package/dist/client/index.js +43 -3
  8. package/dist/client/index.js.map +1 -0
  9. package/dist/{errors-BtORdtK2.js → errors-BOrlo5G2.js} +2 -2
  10. package/dist/{errors-BtORdtK2.js.map → errors-BOrlo5G2.js.map} +1 -1
  11. package/dist/{http-provider-DLJYM4l8.js → http-provider-BWVLNpZn.js} +7 -3
  12. package/dist/http-provider-BWVLNpZn.js.map +1 -0
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/server/index.d.ts +20 -6
  16. package/dist/server/index.js +120 -4
  17. package/dist/server/index.js.map +1 -1
  18. package/dist/{shared-DOEVSpBX.js → shared-Dzp1W1tL.js} +3 -3
  19. package/dist/{shared-DOEVSpBX.js.map → shared-Dzp1W1tL.js.map} +1 -1
  20. package/dist/testing/index.d.ts +19 -2
  21. package/dist/testing/index.js +142 -9
  22. package/dist/testing/index.js.map +1 -1
  23. package/dist/{wire-cYO4W7GH.js → wire-CPO4E3P9.js} +11 -2
  24. package/dist/wire-CPO4E3P9.js.map +1 -0
  25. package/dist/{wire-BJ4-EWvR.d.ts → wire-JnPcAbtN.d.ts} +8 -2
  26. package/package.json +4 -4
  27. package/prompts/operations.md +3 -1
  28. package/prompts/translate.md +4 -0
  29. package/schema/template-content.schema.json +185 -0
  30. package/dist/checks-B-TtaywP.js +0 -646
  31. package/dist/checks-B-TtaywP.js.map +0 -1
  32. package/dist/http-provider-DLJYM4l8.js.map +0 -1
  33. package/dist/wire-cYO4W7GH.js.map +0 -1
@@ -1,6 +1,6 @@
1
- import { i as AI_LIMITS, n as AI_ERROR_STATUS, o as AI_SESSION_HEADER, r as AI_GENERATE_FIELDS, s as AI_WIRE_HEADER, t as AI_ERROR_MESSAGES, u as matchAiRoute } from "../wire-cYO4W7GH.js";
2
- import { C as mergeTagTokens, E as FIX_RULE_GUIDANCE, S as flattenMergeTags, T as unknownMergeTags, _ as buildEditingPrompt, a as checkRewrite, b as editingInstructions, c as createEditingSession, d as opRefusal, f as contentRuleRefusal, g as buildChatRequest, h as buildAltTextRequest, i as checkAltTextAnswer, l as applyOperationsTool, m as REWRITE_PRESET_INSTRUCTIONS, n as buildRewriteRepairPrompt, o as runEditingTurn, p as walkBlocks, r as checkAltText, s as EDITING_SESSION_LIMITS, t as buildAltTextRepairRequest, u as describeOp, v as buildFixRequest, w as mergeTagTokensIn, x as rewriteInstructions, y as buildRewritePrompt } from "../checks-B-TtaywP.js";
3
- import { CollaborationConflict, SdkError, SdkError as SdkError$1, ThrottledError, ThrottledError as ThrottledError$1 } from "@templatical/types";
1
+ import { i as AI_LIMITS, n as AI_ERROR_STATUS, o as AI_SESSION_HEADER, r as AI_GENERATE_FIELDS, s as AI_WIRE_HEADER, t as AI_ERROR_MESSAGES, u as matchAiRoute } from "../wire-CPO4E3P9.js";
2
+ import { A as TRANSLATE_INSTRUCTIONS, C as rewriteInstructions, D as unknownMergeTags, E as mergeTagTokensIn, O as fill, S as languageName, T as mergeTagTokens, _ as buildChatRequest, a as checkRewrite, b as buildRewritePrompt, c as EDITING_SESSION_LIMITS, d as describeOp, f as opRefusal, g as buildAltTextRequest, h as REWRITE_PRESET_INSTRUCTIONS, i as checkAltTextAnswer, k as FIX_RULE_GUIDANCE, l as createEditingSession, m as walkBlocks, n as buildRewriteRepairPrompt, o as stripFences, p as contentRuleRefusal, r as checkAltText, s as runEditingTurn, t as buildAltTextRepairRequest, u as applyOperationsTool, v as buildEditingPrompt, w as flattenMergeTags, x as editingInstructions, y as buildFixRequest } from "../checks-BM_JnvRo.js";
3
+ import { CollaborationConflict, SdkError, SdkError as SdkError$1, ThrottledError, ThrottledError as ThrottledError$1, isLocaleTag, unmaskTags } from "@templatical/types";
4
4
  // src/server/errors.ts
5
5
  const PROVIDER_ERROR = {
6
6
  code: "ai_provider_error",
@@ -212,6 +212,11 @@ async function readAiRequest(request) {
212
212
  };
213
213
  if (base.sessionId !== null && !isBoundedText(base.sessionId, 200)) invalid("The session id must be at most 200 characters.");
214
214
  switch (match.route) {
215
+ case "translate": return {
216
+ ...base,
217
+ route: "translate",
218
+ body: await readTranslationRequest(request)
219
+ };
215
220
  case "conversationMessages":
216
221
  case "clearConversation": return {
217
222
  ...base,
@@ -250,6 +255,7 @@ async function readAiRequest(request) {
250
255
  }
251
256
  };
252
257
  body = {
258
+ locale: json(AI_GENERATE_FIELDS.locale),
253
259
  message: form.get(AI_GENERATE_FIELDS.message),
254
260
  selectedBlockId: form.get(AI_GENERATE_FIELDS.selectedBlockId),
255
261
  revision: form.get(AI_GENERATE_FIELDS.revision),
@@ -304,6 +310,7 @@ async function readAiRequest(request) {
304
310
  ...base,
305
311
  route: "generate",
306
312
  body: {
313
+ locale: locale(body.locale),
307
314
  message,
308
315
  attachments,
309
316
  selectedBlockId: typeof body.selectedBlockId === "string" ? body.selectedBlockId : null,
@@ -317,6 +324,7 @@ async function readAiRequest(request) {
317
324
  ...base,
318
325
  route: "fixFinding",
319
326
  body: {
327
+ locale: locale(body.locale),
320
328
  finding: finding(body.finding),
321
329
  mergeTags: mergeTags(body.mergeTags),
322
330
  content: content(body.content),
@@ -337,6 +345,7 @@ async function readAiRequest(request) {
337
345
  ...base,
338
346
  route: "rewriteText",
339
347
  body: {
348
+ locale: locale(body.locale),
340
349
  html,
341
350
  preset,
342
351
  language,
@@ -347,6 +356,113 @@ async function readAiRequest(request) {
347
356
  }
348
357
  }
349
358
  }
350
- export { EDITING_SESSION_LIMITS, FIX_RULE_GUIDANCE, REWRITE_PRESET_INSTRUCTIONS, SdkError, ThrottledError, aiErrorEvent, aiErrorResponse, aiEventStream, aiJsonResponse, applyOperationsTool, buildAltTextRepairRequest, buildAltTextRequest, buildChatRequest, buildEditingPrompt, buildFixRequest, buildRewritePrompt, buildRewriteRepairPrompt, checkAltText, checkAltTextAnswer, checkRewrite, contentRuleRefusal, createEditingSession, describeOp, editingInstructions, flattenMergeTags, mergeTagTokens, mergeTagTokensIn, opRefusal, readAiRequest, rewriteInstructions, runEditingTurn, unknownMergeTags, walkBlocks };
359
+ function locale(value) {
360
+ if (value === void 0 || value === null) return null;
361
+ if (!isLocaleTag(value)) invalid("locale must be a BCP 47 tag.");
362
+ return value;
363
+ }
364
+ /** Validate a translation body, including direct calls to `translateFields`. */
365
+ function checkTranslationRequest(value) {
366
+ if (!isRecord(value)) invalid("Send a JSON object.");
367
+ if (value.templateId !== null && (typeof value.templateId !== "string" || value.templateId === "")) invalid("templateId must be a non-empty string or null.");
368
+ for (const key of ["sourceLocale", "targetLocale"]) if (locale(value[key]) === null) invalid(`${key} must be a BCP 47 tag.`);
369
+ if (!Array.isArray(value.fields) || value.fields.length === 0) invalid("fields must be a non-empty array.");
370
+ if (value.fields.length > AI_LIMITS.translationMaxFields) throw new SdkError$1(AI_ERROR_MESSAGES.content_too_large, 413, "content_too_large");
371
+ const ids = /* @__PURE__ */ new Set();
372
+ let chars = 0;
373
+ for (const field of value.fields) {
374
+ if (!isRecord(field) || typeof field.id !== "string" || field.id === "" || ids.has(field.id) || field.format !== "html" && field.format !== "text" || typeof field.text !== "string") invalid("Each field must have a unique non-empty id, html or text format, and string text.");
375
+ ids.add(field.id);
376
+ chars += field.text.length;
377
+ }
378
+ if (chars > AI_LIMITS.translationMaxChars) throw new SdkError$1(AI_ERROR_MESSAGES.content_too_large, 413, "content_too_large");
379
+ return value;
380
+ }
381
+ /** Read the JSON translation body and enforce the translation call limits. */
382
+ async function readTranslationRequest(request) {
383
+ return checkTranslationRequest(await request.json().catch(() => invalid("Send a JSON body.")));
384
+ }
385
+ // src/server/translate.ts
386
+ function passingFields(answer, request) {
387
+ let parsed;
388
+ try {
389
+ parsed = JSON.parse(stripFences(answer).trim());
390
+ } catch {
391
+ return [];
392
+ }
393
+ if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.fields)) return [];
394
+ const fields = parsed.fields;
395
+ return request.fields.flatMap((source) => {
396
+ const matches = fields.filter((field) => typeof field === "object" && field !== null && "id" in field && field.id === source.id);
397
+ if (matches.length !== 1 || typeof matches[0].text !== "string") return [];
398
+ const text = matches[0].text;
399
+ const tokens = [...source.text.matchAll(/<x id="(\d+)"\s*\/>|<x id="(\d+)"><\/x>|__x(\d+)__/g)].map(() => "");
400
+ if (unmaskTags(source.text, tokens) === null || unmaskTags(text, tokens) === null) return [];
401
+ return [{
402
+ id: source.id,
403
+ text
404
+ }];
405
+ });
406
+ }
407
+ /** Translate masked fields, with one repair and a partial result for fields that pass. */
408
+ async function translateFields(options) {
409
+ const { model, signal } = options;
410
+ signal?.throwIfAborted();
411
+ const request = checkTranslationRequest(options.request);
412
+ const system = fill(TRANSLATE_INSTRUCTIONS, {
413
+ source: `${languageName(request.sourceLocale)} (${request.sourceLocale})`,
414
+ target: `${languageName(request.targetLocale)} (${request.targetLocale})`
415
+ });
416
+ const messages = [{
417
+ role: "user",
418
+ content: `Translate from ${languageName(request.sourceLocale)} (${request.sourceLocale}) to ${languageName(request.targetLocale)} (${request.targetLocale}).\n\nFields:\n${JSON.stringify(request.fields)}`
419
+ }];
420
+ const accepted = /* @__PURE__ */ new Map();
421
+ for (let attempt = 0; attempt < 2; attempt++) {
422
+ signal?.throwIfAborted();
423
+ let answer = "";
424
+ try {
425
+ for await (const part of model.stream({
426
+ system,
427
+ messages,
428
+ tools: [],
429
+ signal
430
+ })) {
431
+ signal?.throwIfAborted();
432
+ if (part.type === "text") answer += part.text;
433
+ }
434
+ } catch (error) {
435
+ signal?.throwIfAborted();
436
+ if (error instanceof Error && error.name === "AbortError" || typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && [
437
+ "rate_limited",
438
+ "limit_reached",
439
+ "content_too_large",
440
+ "translation_failed"
441
+ ].includes(error.code)) throw error;
442
+ if (!(error instanceof SdkError$1) || error.code !== "ai_provider_error" && error.code !== "ai_output_invalid") throw new SdkError$1("The translation couldn't produce a valid result. Try again.", 502, "translation_failed");
443
+ }
444
+ signal?.throwIfAborted();
445
+ const pending = {
446
+ ...request,
447
+ fields: request.fields.filter((field) => !accepted.has(field.id))
448
+ };
449
+ for (const field of passingFields(answer, pending)) accepted.set(field.id, field.text);
450
+ const failed = request.fields.filter((field) => !accepted.has(field.id)).map((field) => field.id);
451
+ if (failed.length === 0) break;
452
+ messages.push({
453
+ role: "assistant",
454
+ content: answer
455
+ }, {
456
+ role: "user",
457
+ content: `Return complete JSON {"fields":[{"id":"…","text":"…"}]} for these failed ids only: ${JSON.stringify(failed)}. Preserve every placeholder exactly once.`
458
+ });
459
+ }
460
+ if (accepted.size === 0) throw new SdkError$1("The translation couldn't produce a valid result. Try again.", 502, "translation_failed");
461
+ return { fields: request.fields.flatMap(({ id }) => accepted.has(id) ? [{
462
+ id,
463
+ text: accepted.get(id)
464
+ }] : []) };
465
+ }
466
+ export { EDITING_SESSION_LIMITS, FIX_RULE_GUIDANCE, REWRITE_PRESET_INSTRUCTIONS, SdkError, ThrottledError, aiErrorEvent, aiErrorResponse, aiEventStream, aiJsonResponse, applyOperationsTool, buildAltTextRepairRequest, buildAltTextRequest, buildChatRequest, buildEditingPrompt, buildFixRequest, buildRewritePrompt, buildRewriteRepairPrompt, checkAltText, checkAltTextAnswer, checkRewrite, contentRuleRefusal, createEditingSession, describeOp, editingInstructions, flattenMergeTags, mergeTagTokens, mergeTagTokensIn, opRefusal, readAiRequest, readTranslationRequest, rewriteInstructions, runEditingTurn, translateFields, unknownMergeTags, walkBlocks };
351
467
 
352
468
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["SdkError","ThrottledError","SdkError"],"sources":["../../src/server/errors.ts","../../src/server/event-stream.ts","../../src/server/read-request.ts"],"sourcesContent":["import {\n CollaborationConflict,\n SdkError,\n ThrottledError,\n} from \"@templatical/types\";\nimport {\n AI_ERROR_MESSAGES,\n AI_ERROR_STATUS,\n AI_WIRE_HEADER,\n AI_WIRE_VERSION,\n type AiErrorEvent,\n} from \"../wire\";\n\nconst PROVIDER_ERROR = {\n code: \"ai_provider_error\",\n message: AI_ERROR_MESSAGES.ai_provider_error,\n} as const;\n\nfunction known(error: unknown): error is SdkError & { code: string } {\n return error instanceof SdkError && typeof error.code === \"string\";\n}\n\nfunction conflictDetails(error: unknown): Record<string, unknown> {\n if (!(error instanceof CollaborationConflict)) return {};\n return {\n ...(error.opIndex !== undefined ? { opIndex: error.opIndex } : {}),\n ...(error.holder !== undefined ? { holder: error.holder } : {}),\n ...(error.holders !== undefined ? { holders: error.holders } : {}),\n };\n}\n\n/** An error as the stream's last event. Anything but a coded `SdkError` hides behind the provider error. */\nexport function aiErrorEvent(error: unknown): AiErrorEvent {\n if (!known(error)) return { type: \"error\", ...PROVIDER_ERROR };\n return {\n ...error.details,\n ...conflictDetails(error),\n type: \"error\",\n code: error.code,\n message: error.message,\n };\n}\n\n/** A JSON response with the wire's header; `204` has no body. */\nexport function aiJsonResponse(body: unknown, status = 200): Response {\n const headers = new Headers({ [AI_WIRE_HEADER]: AI_WIRE_VERSION });\n if (status === 204) return new Response(null, { status, headers });\n headers.set(\"Content-Type\", \"application/json\");\n return new Response(JSON.stringify(body), { status, headers });\n}\n\n/**\n * An error before the stream starts: `{ message, code, …details }` with the\n * code's status, `Retry-After` (seconds) for a `ThrottledError`.\n */\nexport function aiErrorResponse(error: unknown): Response {\n if (!(error instanceof SdkError))\n return aiJsonResponse({ ...PROVIDER_ERROR }, 502);\n // A validation failure (`readAiRequest`'s 422) has no code, as Laravel's has none.\n const code = typeof error.code === \"string\" ? error.code : undefined;\n const status =\n error.statusCode ??\n (code !== undefined ? AI_ERROR_STATUS[code] : undefined) ??\n 400;\n const response = aiJsonResponse(\n {\n ...error.details,\n ...conflictDetails(error),\n message: error.message,\n ...(code !== undefined ? { code } : {}),\n },\n status,\n );\n if (error instanceof ThrottledError) {\n response.headers.set(\n \"Retry-After\",\n String(Math.ceil(error.retryAfterMs / 1000)),\n );\n }\n return response;\n}\n\nexport { PROVIDER_ERROR };\n","import { AI_WIRE_HEADER, AI_WIRE_VERSION, type AiWireEvent } from \"../wire\";\nimport { aiErrorEvent, PROVIDER_ERROR } from \"./errors\";\n\nexport interface AiEventStreamController {\n /** Write an event. Ignored once `done` or `error` was written. */\n send(event: AiWireEvent): void;\n /** Aborted when the client goes away or `options.signal` aborts. */\n readonly signal: AbortSignal;\n}\n\nexport interface AiEventStreamOptions {\n /** Write `: ping` after this long without a write (15 s). */\n pingMs?: number;\n signal?: AbortSignal;\n headers?: HeadersInit;\n}\n\n/**\n * The SSE response for any Fetch-API server (Next, Hono, Workers, Deno, Bun;\n * Express through an adapter). The producer writes events; a producer that\n * throws ends the stream with an `error` event, and one that returns without\n * `done` or `error` ends it with the provider error.\n */\nexport function aiEventStream(\n producer: (stream: AiEventStreamController) => Promise<void>,\n options: AiEventStreamOptions = {},\n): Response {\n const pingMs = options.pingMs ?? 15_000;\n const encoder = new TextEncoder();\n const abort = new AbortController();\n function forwardAbort(): void {\n abort.abort(options.signal?.reason);\n }\n if (options.signal) {\n if (options.signal.aborted) abort.abort(options.signal.reason);\n else options.signal.addEventListener(\"abort\", forwardAbort, { once: true });\n }\n let timer: ReturnType<typeof setInterval> | null = null;\n let closed = false;\n\n function cleanup(): void {\n if (timer) clearInterval(timer);\n options.signal?.removeEventListener(\"abort\", forwardAbort);\n }\n\n const body = new ReadableStream<Uint8Array>({\n start(controller) {\n let finished = false;\n let lastWrite = Date.now();\n const write = (chunk: string): void => {\n if (closed) return;\n controller.enqueue(encoder.encode(chunk));\n lastWrite = Date.now();\n };\n const send = (event: AiWireEvent): void => {\n if (finished) return;\n write(`data: ${JSON.stringify(event)}\\n\\n`);\n if (event.type === \"done\" || event.type === \"error\") finished = true;\n };\n timer = setInterval(\n () => {\n if (Date.now() - lastWrite >= pingMs) write(\": ping\\n\\n\");\n },\n Math.max(1, Math.floor(pingMs / 3)),\n );\n void (async () => {\n try {\n await producer({ send, signal: abort.signal });\n if (!abort.signal.aborted) send({ type: \"error\", ...PROVIDER_ERROR });\n } catch (error) {\n if (!abort.signal.aborted) send(aiErrorEvent(error));\n } finally {\n cleanup();\n if (!closed) {\n closed = true;\n controller.close();\n }\n }\n })();\n },\n cancel(reason) {\n closed = true;\n cleanup();\n abort.abort(reason);\n },\n });\n\n const headers = new Headers(options.headers);\n headers.set(\"Content-Type\", \"text/event-stream\");\n headers.set(\"Cache-Control\", \"no-cache\");\n headers.set(\"X-Accel-Buffering\", \"no\");\n headers.set(AI_WIRE_HEADER, AI_WIRE_VERSION);\n return new Response(body, { headers });\n}\n","import type {\n AiFixFinding,\n AiChatAttachment,\n AiMergeTags,\n AiRewritePreset,\n TemplateContent,\n} from \"@templatical/types\";\nimport { SdkError } from \"@templatical/types\";\nimport {\n AI_GENERATE_FIELDS,\n AI_ERROR_MESSAGES,\n AI_LIMITS,\n AI_SESSION_HEADER,\n matchAiRoute,\n type AiFixFindingBody,\n type AiAltTextBody,\n type AiGenerateBody,\n type AiRewriteBody,\n} from \"../wire\";\n\ninterface Base {\n template: string;\n /** `X-Templatical-Session`, when sent. */\n sessionId: string | null;\n}\n\nexport type AiWireRequest = Base &\n (\n | { route: \"generate\"; body: AiGenerateBody }\n | { route: \"fixFinding\"; body: AiFixFindingBody }\n | { route: \"rewriteText\"; body: AiRewriteBody }\n | { route: \"altText\"; body: AiAltTextBody }\n | { route: \"conversationMessages\" | \"clearConversation\" }\n | { route: \"undoMessage\" | \"redoMessage\"; message: string }\n | {\n route: \"settleReplay\";\n message: string;\n replayId: string;\n body: { direction: \"undo\" | \"redo\"; applied: boolean };\n }\n );\n\nconst PRESETS: readonly AiRewritePreset[] = [\n \"shorten\",\n \"expand\",\n \"friendlier\",\n \"formal\",\n \"fixGrammar\",\n \"translate\",\n];\nconst IMAGE_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"];\nconst MERGE_TAG_SYNTAXES = [\n \"liquid\",\n \"handlebars\",\n \"mailchimp\",\n \"ampscript\",\n \"custom\",\n];\n\nfunction invalid(message: string): never {\n throw new SdkError(message, 422);\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isBoundedText = (value: unknown, max: number): value is string =>\n typeof value === \"string\" && Array.from(value).length <= max;\n\nfunction content(value: unknown): TemplateContent {\n if (\n !isRecord(value) ||\n !Array.isArray(value.blocks) ||\n !isRecord(value.settings)\n ) {\n invalid(\"content must be the template's content.\");\n }\n if (\n new TextEncoder().encode(JSON.stringify(value)).length >\n AI_LIMITS.contentMaxBytes\n )\n throw new SdkError(\n AI_ERROR_MESSAGES.content_too_large,\n 413,\n \"content_too_large\",\n );\n return value as unknown as TemplateContent;\n}\n\nfunction mergeTags(value: unknown): AiMergeTags {\n if (\n !isRecord(value) ||\n typeof value.syntax !== \"string\" ||\n !MERGE_TAG_SYNTAXES.includes(value.syntax) ||\n !Array.isArray(value.tags) ||\n value.tags.length > AI_LIMITS.mergeTagsMax\n ) {\n invalid(\"mergeTags must be { syntax, tags } with at most 500 tags.\");\n }\n for (const tag of value.tags) {\n if (\n !isRecord(tag) ||\n !isBoundedText(tag.value, 200) ||\n !isBoundedText(tag.label, 100) ||\n (tag.group !== undefined && !isBoundedText(tag.group, 100)) ||\n (tag.description !== undefined && !isBoundedText(tag.description, 200))\n ) {\n invalid(\n \"Each merge tag must have string value (at most 200 characters) and label (at most 100), with optional string group (at most 100) and description (at most 200).\",\n );\n }\n }\n return value as unknown as AiMergeTags;\n}\n\nfunction revision(value: unknown): number | null {\n if (value === null || value === undefined || value === \"\") return null;\n const number = typeof value === \"string\" ? Number(value) : value;\n if (typeof number !== \"number\" || !Number.isInteger(number) || number < 0) {\n invalid(\"revision must be a whole number or null.\");\n }\n return number;\n}\n\nfunction optionalText(\n value: unknown,\n max: number,\n message: string,\n): string | null {\n if (value === null || value === undefined || value === \"\") return null;\n if (typeof value !== \"string\" || Array.from(value).length > max)\n invalid(message);\n return value;\n}\n\nfunction finding(value: unknown): AiFixFinding {\n if (!isRecord(value))\n invalid(\n \"finding must be { ruleId, message, suggestion, reference, blockId, excerpt? }.\",\n );\n if (\n typeof value.ruleId !== \"string\" ||\n value.ruleId.length === 0 ||\n value.ruleId.length > 200\n )\n invalid(\"finding.ruleId must be 1 to 200 characters.\");\n if (!isBoundedText(value.message, 1000))\n invalid(\"finding.message must be at most 1,000 code points.\");\n if (!isBoundedText(value.suggestion, 1000))\n invalid(\"finding.suggestion must be at most 1,000 code points.\");\n let reference: URL | null = null;\n if (typeof value.reference === \"string\" && value.reference.length <= 2048) {\n try {\n reference = new URL(value.reference);\n } catch {\n /* Invalid URL. */\n }\n }\n if (\n !reference ||\n (reference.protocol !== \"http:\" && reference.protocol !== \"https:\")\n )\n invalid(\n \"finding.reference must be an http: or https: URL of at most 2,048 characters.\",\n );\n if (\n value.blockId !== null &&\n (typeof value.blockId !== \"string\" || value.blockId.length > 200)\n )\n invalid(\n \"finding.blockId must be a string of at most 200 characters or null.\",\n );\n if (value.excerpt !== undefined && !isBoundedText(value.excerpt, 1000))\n invalid(\"finding.excerpt must be at most 1,000 code points when present.\");\n return value as unknown as AiFixFinding;\n}\n\n/**\n * The wire's request, parsed and checked against `AI_LIMITS`; `null` when the\n * request is no route of the wire. Field validation throws `SdkError(message, 422)`;\n * oversized content throws `content_too_large` (413).\n */\nexport async function readAiRequest(\n request: Request,\n): Promise<AiWireRequest | null> {\n const match = matchAiRoute(request.method, new URL(request.url).pathname);\n if (!match) return null;\n const base: Base = {\n template: match.template,\n sessionId: request.headers.get(AI_SESSION_HEADER),\n };\n\n if (base.sessionId !== null && !isBoundedText(base.sessionId, 200))\n invalid(\"The session id must be at most 200 characters.\");\n\n switch (match.route) {\n case \"conversationMessages\":\n case \"clearConversation\":\n return { ...base, route: match.route };\n case \"undoMessage\":\n case \"redoMessage\":\n return { ...base, route: match.route, message: match.message ?? \"\" };\n default:\n break;\n }\n\n let attachments: AiChatAttachment[] = [];\n let body: unknown;\n if (\n match.route === \"generate\" &&\n request.headers.get(\"Content-Type\")?.startsWith(\"multipart/form-data\")\n ) {\n const form = await request\n .formData()\n .catch(() => invalid(\"Send the message as multipart form data.\"));\n const files = form.getAll(AI_GENERATE_FIELDS.attachment);\n if (files.length > AI_LIMITS.attachmentsMax)\n invalid(\"Attach at most 4 files.\");\n if (\n files.some(\n (file) =>\n !(file instanceof Blob) ||\n (!IMAGE_TYPES.includes(file.type) && file.type !== \"application/pdf\"),\n )\n )\n invalid(\"Attach PNG, JPEG, WebP or GIF images, or PDFs.\");\n const blobs = files as File[];\n if (blobs.some((file) => file.size > AI_LIMITS.attachmentMaxBytes))\n invalid(\"Each attachment must be 5 MB or smaller.\");\n if (\n blobs.reduce((total, file) => total + file.size, 0) >\n AI_LIMITS.attachmentsTotalMaxBytes\n )\n invalid(\"Attachments must be 10 MB or smaller together.\");\n if (blobs.some((file) => file.name.length > 255))\n invalid(\"An attachment's file name must be at most 255 characters.\");\n attachments = blobs.map((file) => ({\n file,\n kind: file.type === \"application/pdf\" ? \"pdf\" : \"image\",\n name: file.name,\n }));\n const json = (field: string): unknown => {\n const raw = form.get(field);\n if (raw === null) return null;\n try {\n return JSON.parse(String(raw));\n } catch {\n return invalid(`${field} must be JSON.`);\n }\n };\n body = {\n message: form.get(AI_GENERATE_FIELDS.message),\n selectedBlockId: form.get(AI_GENERATE_FIELDS.selectedBlockId),\n revision: form.get(AI_GENERATE_FIELDS.revision),\n mergeTags: json(AI_GENERATE_FIELDS.mergeTags),\n content: json(AI_GENERATE_FIELDS.content),\n };\n } else {\n body = await request.json().catch(() => invalid(\"Send a JSON body.\"));\n }\n if (!isRecord(body)) invalid(\"Send a JSON object.\");\n\n switch (match.route) {\n case \"altText\": {\n if (\n typeof body.blockId !== \"string\" ||\n body.blockId.length === 0 ||\n body.blockId.length > 200\n )\n invalid(\"blockId must be 1 to 200 characters.\");\n let url: URL | null = null;\n if (\n isRecord(body.image) &&\n typeof body.image.url === \"string\" &&\n body.image.url.length <= 2048\n ) {\n try {\n url = new URL(body.image.url);\n } catch {\n /* Invalid URL. */\n }\n }\n if (!url || (url.protocol !== \"http:\" && url.protocol !== \"https:\"))\n invalid(\n \"image.url must be an http: or https: URL of at most 2,048 characters.\",\n );\n for (const key of [\"currentAlt\", \"linkHref\"] as const) {\n if (\n body[key] !== null &&\n (typeof body[key] !== \"string\" || body[key].length > 2048)\n )\n invalid(\n `${key} must be a string of at most 2,048 characters or null.`,\n );\n }\n if (!isBoundedText(body.nearbyText, 2000))\n invalid(\"nearbyText must be at most 2,000 code points.\");\n if (\n typeof body.locale !== \"string\" ||\n body.locale.length > 35 ||\n !/^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$/.test(body.locale)\n )\n invalid(\"locale must be a BCP 47 tag of at most 35 characters.\");\n if (\n typeof body.maxLength !== \"number\" ||\n !Number.isInteger(body.maxLength) ||\n body.maxLength < 1 ||\n body.maxLength > 1000\n )\n invalid(\"maxLength must be an integer from 1 to 1,000.\");\n return {\n ...base,\n route: \"altText\",\n body: {\n blockId: body.blockId,\n image: { url: (body.image as { url: string }).url },\n currentAlt: body.currentAlt as string | null,\n linkHref: body.linkHref as string | null,\n nearbyText: body.nearbyText,\n locale: body.locale,\n maxLength: body.maxLength,\n },\n };\n }\n case \"settleReplay\": {\n if (\n (body.direction !== \"undo\" && body.direction !== \"redo\") ||\n typeof body.applied !== \"boolean\"\n )\n invalid(\n \"A replay outcome must have direction undo or redo and boolean applied.\",\n );\n return {\n ...base,\n route: \"settleReplay\",\n message: match.message!,\n replayId: match.replayId!,\n body: { direction: body.direction, applied: body.applied },\n };\n }\n case \"generate\": {\n const message = body.message;\n if (\n typeof message !== \"string\" ||\n message.trim() === \"\" ||\n Array.from(message).length > AI_LIMITS.messageMaxChars\n ) {\n invalid(\"The message must be 1 to 4,000 characters.\");\n }\n if (\n typeof body.selectedBlockId === \"string\" &&\n !isBoundedText(body.selectedBlockId, 200)\n )\n invalid(\"selectedBlockId must be at most 200 characters.\");\n return {\n ...base,\n route: \"generate\",\n body: {\n message,\n attachments,\n selectedBlockId:\n typeof body.selectedBlockId === \"string\"\n ? body.selectedBlockId\n : null,\n mergeTags: mergeTags(body.mergeTags),\n content: content(body.content),\n revision: revision(body.revision),\n },\n };\n }\n case \"fixFinding\":\n return {\n ...base,\n route: \"fixFinding\",\n body: {\n finding: finding(body.finding),\n mergeTags: mergeTags(body.mergeTags),\n content: content(body.content),\n revision: revision(body.revision),\n },\n };\n case \"rewriteText\": {\n const html = body.html;\n if (\n typeof html !== \"string\" ||\n html === \"\" ||\n Array.from(html).length > AI_LIMITS.rewriteHtmlMaxChars\n ) {\n invalid(\"html must be 1 to 20,000 characters.\");\n }\n const preset = body.preset ?? null;\n if (preset !== null && !PRESETS.includes(preset as AiRewritePreset))\n invalid(\"Unknown preset.\");\n const instruction = optionalText(\n body.instruction,\n AI_LIMITS.instructionMaxChars,\n \"The instruction must be at most 500 characters.\",\n );\n if (preset === null && instruction === null)\n invalid(\"Choose a preset or write an instruction.\");\n const language = body.language === \"\" ? null : (body.language ?? null);\n if (\n language !== null &&\n (typeof language !== \"string\" ||\n !/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/.test(language))\n ) {\n invalid(\"language must be a BCP 47 tag.\");\n }\n if (preset === \"translate\" && language === null)\n invalid(\"Choose a language to translate into.\");\n return {\n ...base,\n route: \"rewriteText\",\n body: {\n html,\n preset: preset as AiRewritePreset | null,\n language,\n instruction,\n mergeTags: mergeTags(body.mergeTags),\n },\n };\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["SdkError","ThrottledError","SdkError","SdkError"],"sources":["../../src/server/errors.ts","../../src/server/event-stream.ts","../../src/server/read-request.ts","../../src/server/translate.ts"],"sourcesContent":["import {\n CollaborationConflict,\n SdkError,\n ThrottledError,\n} from \"@templatical/types\";\nimport {\n AI_ERROR_MESSAGES,\n AI_ERROR_STATUS,\n AI_WIRE_HEADER,\n AI_WIRE_VERSION,\n type AiErrorEvent,\n} from \"../wire\";\n\nconst PROVIDER_ERROR = {\n code: \"ai_provider_error\",\n message: AI_ERROR_MESSAGES.ai_provider_error,\n} as const;\n\nfunction known(error: unknown): error is SdkError & { code: string } {\n return error instanceof SdkError && typeof error.code === \"string\";\n}\n\nfunction conflictDetails(error: unknown): Record<string, unknown> {\n if (!(error instanceof CollaborationConflict)) return {};\n return {\n ...(error.opIndex !== undefined ? { opIndex: error.opIndex } : {}),\n ...(error.holder !== undefined ? { holder: error.holder } : {}),\n ...(error.holders !== undefined ? { holders: error.holders } : {}),\n };\n}\n\n/** An error as the stream's last event. Anything but a coded `SdkError` hides behind the provider error. */\nexport function aiErrorEvent(error: unknown): AiErrorEvent {\n if (!known(error)) return { type: \"error\", ...PROVIDER_ERROR };\n return {\n ...error.details,\n ...conflictDetails(error),\n type: \"error\",\n code: error.code,\n message: error.message,\n };\n}\n\n/** A JSON response with the wire's header; `204` has no body. */\nexport function aiJsonResponse(body: unknown, status = 200): Response {\n const headers = new Headers({ [AI_WIRE_HEADER]: AI_WIRE_VERSION });\n if (status === 204) return new Response(null, { status, headers });\n headers.set(\"Content-Type\", \"application/json\");\n return new Response(JSON.stringify(body), { status, headers });\n}\n\n/**\n * An error before the stream starts: `{ message, code, …details }` with the\n * code's status, `Retry-After` (seconds) for a `ThrottledError`.\n */\nexport function aiErrorResponse(error: unknown): Response {\n if (!(error instanceof SdkError))\n return aiJsonResponse({ ...PROVIDER_ERROR }, 502);\n // A validation failure (`readAiRequest`'s 422) has no code, as Laravel's has none.\n const code = typeof error.code === \"string\" ? error.code : undefined;\n const status =\n error.statusCode ??\n (code !== undefined ? AI_ERROR_STATUS[code] : undefined) ??\n 400;\n const response = aiJsonResponse(\n {\n ...error.details,\n ...conflictDetails(error),\n message: error.message,\n ...(code !== undefined ? { code } : {}),\n },\n status,\n );\n if (error instanceof ThrottledError) {\n response.headers.set(\n \"Retry-After\",\n String(Math.ceil(error.retryAfterMs / 1000)),\n );\n }\n return response;\n}\n\nexport { PROVIDER_ERROR };\n","import { AI_WIRE_HEADER, AI_WIRE_VERSION, type AiWireEvent } from \"../wire\";\nimport { aiErrorEvent, PROVIDER_ERROR } from \"./errors\";\n\nexport interface AiEventStreamController {\n /** Write an event. Ignored once `done` or `error` was written. */\n send(event: AiWireEvent): void;\n /** Aborted when the client goes away or `options.signal` aborts. */\n readonly signal: AbortSignal;\n}\n\nexport interface AiEventStreamOptions {\n /** Write `: ping` after this long without a write (15 s). */\n pingMs?: number;\n signal?: AbortSignal;\n headers?: HeadersInit;\n}\n\n/**\n * The SSE response for any Fetch-API server (Next, Hono, Workers, Deno, Bun;\n * Express through an adapter). The producer writes events; a producer that\n * throws ends the stream with an `error` event, and one that returns without\n * `done` or `error` ends it with the provider error.\n */\nexport function aiEventStream(\n producer: (stream: AiEventStreamController) => Promise<void>,\n options: AiEventStreamOptions = {},\n): Response {\n const pingMs = options.pingMs ?? 15_000;\n const encoder = new TextEncoder();\n const abort = new AbortController();\n function forwardAbort(): void {\n abort.abort(options.signal?.reason);\n }\n if (options.signal) {\n if (options.signal.aborted) abort.abort(options.signal.reason);\n else options.signal.addEventListener(\"abort\", forwardAbort, { once: true });\n }\n let timer: ReturnType<typeof setInterval> | null = null;\n let closed = false;\n\n function cleanup(): void {\n if (timer) clearInterval(timer);\n options.signal?.removeEventListener(\"abort\", forwardAbort);\n }\n\n const body = new ReadableStream<Uint8Array>({\n start(controller) {\n let finished = false;\n let lastWrite = Date.now();\n const write = (chunk: string): void => {\n if (closed) return;\n controller.enqueue(encoder.encode(chunk));\n lastWrite = Date.now();\n };\n const send = (event: AiWireEvent): void => {\n if (finished) return;\n write(`data: ${JSON.stringify(event)}\\n\\n`);\n if (event.type === \"done\" || event.type === \"error\") finished = true;\n };\n timer = setInterval(\n () => {\n if (Date.now() - lastWrite >= pingMs) write(\": ping\\n\\n\");\n },\n Math.max(1, Math.floor(pingMs / 3)),\n );\n void (async () => {\n try {\n await producer({ send, signal: abort.signal });\n if (!abort.signal.aborted) send({ type: \"error\", ...PROVIDER_ERROR });\n } catch (error) {\n if (!abort.signal.aborted) send(aiErrorEvent(error));\n } finally {\n cleanup();\n if (!closed) {\n closed = true;\n controller.close();\n }\n }\n })();\n },\n cancel(reason) {\n closed = true;\n cleanup();\n abort.abort(reason);\n },\n });\n\n const headers = new Headers(options.headers);\n headers.set(\"Content-Type\", \"text/event-stream\");\n headers.set(\"Cache-Control\", \"no-cache\");\n headers.set(\"X-Accel-Buffering\", \"no\");\n headers.set(AI_WIRE_HEADER, AI_WIRE_VERSION);\n return new Response(body, { headers });\n}\n","import type {\n AiFixFinding,\n AiChatAttachment,\n AiMergeTags,\n AiRewritePreset,\n TemplateContent,\n TranslationRequest,\n} from \"@templatical/types\";\nimport { isLocaleTag, SdkError } from \"@templatical/types\";\nimport {\n AI_GENERATE_FIELDS,\n AI_ERROR_MESSAGES,\n AI_LIMITS,\n AI_SESSION_HEADER,\n matchAiRoute,\n type AiFixFindingBody,\n type AiAltTextBody,\n type AiGenerateBody,\n type AiRewriteBody,\n} from \"../wire\";\n\ninterface Base {\n template: string;\n /** `X-Templatical-Session`, when sent. */\n sessionId: string | null;\n}\n\nexport type AiWireRequest = Base &\n (\n | { route: \"translate\"; body: TranslationRequest }\n | { route: \"generate\"; body: AiGenerateBody }\n | { route: \"fixFinding\"; body: AiFixFindingBody }\n | { route: \"rewriteText\"; body: AiRewriteBody }\n | { route: \"altText\"; body: AiAltTextBody }\n | { route: \"conversationMessages\" | \"clearConversation\" }\n | { route: \"undoMessage\" | \"redoMessage\"; message: string }\n | {\n route: \"settleReplay\";\n message: string;\n replayId: string;\n body: { direction: \"undo\" | \"redo\"; applied: boolean };\n }\n );\n\nconst PRESETS: readonly AiRewritePreset[] = [\n \"shorten\",\n \"expand\",\n \"friendlier\",\n \"formal\",\n \"fixGrammar\",\n \"translate\",\n];\nconst IMAGE_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"];\nconst MERGE_TAG_SYNTAXES = [\n \"liquid\",\n \"handlebars\",\n \"mailchimp\",\n \"ampscript\",\n \"custom\",\n];\n\nfunction invalid(message: string): never {\n throw new SdkError(message, 422);\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isBoundedText = (value: unknown, max: number): value is string =>\n typeof value === \"string\" && Array.from(value).length <= max;\n\nfunction content(value: unknown): TemplateContent {\n if (\n !isRecord(value) ||\n !Array.isArray(value.blocks) ||\n !isRecord(value.settings)\n ) {\n invalid(\"content must be the template's content.\");\n }\n if (\n new TextEncoder().encode(JSON.stringify(value)).length >\n AI_LIMITS.contentMaxBytes\n )\n throw new SdkError(\n AI_ERROR_MESSAGES.content_too_large,\n 413,\n \"content_too_large\",\n );\n return value as unknown as TemplateContent;\n}\n\nfunction mergeTags(value: unknown): AiMergeTags {\n if (\n !isRecord(value) ||\n typeof value.syntax !== \"string\" ||\n !MERGE_TAG_SYNTAXES.includes(value.syntax) ||\n !Array.isArray(value.tags) ||\n value.tags.length > AI_LIMITS.mergeTagsMax\n ) {\n invalid(\"mergeTags must be { syntax, tags } with at most 500 tags.\");\n }\n for (const tag of value.tags) {\n if (\n !isRecord(tag) ||\n !isBoundedText(tag.value, 200) ||\n !isBoundedText(tag.label, 100) ||\n (tag.group !== undefined && !isBoundedText(tag.group, 100)) ||\n (tag.description !== undefined && !isBoundedText(tag.description, 200))\n ) {\n invalid(\n \"Each merge tag must have string value (at most 200 characters) and label (at most 100), with optional string group (at most 100) and description (at most 200).\",\n );\n }\n }\n return value as unknown as AiMergeTags;\n}\n\nfunction revision(value: unknown): number | null {\n if (value === null || value === undefined || value === \"\") return null;\n const number = typeof value === \"string\" ? Number(value) : value;\n if (typeof number !== \"number\" || !Number.isInteger(number) || number < 0) {\n invalid(\"revision must be a whole number or null.\");\n }\n return number;\n}\n\nfunction optionalText(\n value: unknown,\n max: number,\n message: string,\n): string | null {\n if (value === null || value === undefined || value === \"\") return null;\n if (typeof value !== \"string\" || Array.from(value).length > max)\n invalid(message);\n return value;\n}\n\nfunction finding(value: unknown): AiFixFinding {\n if (!isRecord(value))\n invalid(\n \"finding must be { ruleId, message, suggestion, reference, blockId, excerpt? }.\",\n );\n if (\n typeof value.ruleId !== \"string\" ||\n value.ruleId.length === 0 ||\n value.ruleId.length > 200\n )\n invalid(\"finding.ruleId must be 1 to 200 characters.\");\n if (!isBoundedText(value.message, 1000))\n invalid(\"finding.message must be at most 1,000 code points.\");\n if (!isBoundedText(value.suggestion, 1000))\n invalid(\"finding.suggestion must be at most 1,000 code points.\");\n let reference: URL | null = null;\n if (typeof value.reference === \"string\" && value.reference.length <= 2048) {\n try {\n reference = new URL(value.reference);\n } catch {\n /* Invalid URL. */\n }\n }\n if (\n !reference ||\n (reference.protocol !== \"http:\" && reference.protocol !== \"https:\")\n )\n invalid(\n \"finding.reference must be an http: or https: URL of at most 2,048 characters.\",\n );\n if (\n value.blockId !== null &&\n (typeof value.blockId !== \"string\" || value.blockId.length > 200)\n )\n invalid(\n \"finding.blockId must be a string of at most 200 characters or null.\",\n );\n if (value.excerpt !== undefined && !isBoundedText(value.excerpt, 1000))\n invalid(\"finding.excerpt must be at most 1,000 code points when present.\");\n return value as unknown as AiFixFinding;\n}\n\n/**\n * The wire's request, parsed and checked against `AI_LIMITS`; `null` when the\n * request is no route of the wire. Field validation throws `SdkError(message, 422)`;\n * oversized content throws `content_too_large` (413).\n */\nexport async function readAiRequest(\n request: Request,\n): Promise<AiWireRequest | null> {\n const match = matchAiRoute(request.method, new URL(request.url).pathname);\n if (!match) return null;\n const base: Base = {\n template: match.template,\n sessionId: request.headers.get(AI_SESSION_HEADER),\n };\n\n if (base.sessionId !== null && !isBoundedText(base.sessionId, 200))\n invalid(\"The session id must be at most 200 characters.\");\n\n switch (match.route) {\n case \"translate\":\n return {\n ...base,\n route: \"translate\",\n body: await readTranslationRequest(request),\n };\n case \"conversationMessages\":\n case \"clearConversation\":\n return { ...base, route: match.route };\n case \"undoMessage\":\n case \"redoMessage\":\n return { ...base, route: match.route, message: match.message ?? \"\" };\n default:\n break;\n }\n\n let attachments: AiChatAttachment[] = [];\n let body: unknown;\n if (\n match.route === \"generate\" &&\n request.headers.get(\"Content-Type\")?.startsWith(\"multipart/form-data\")\n ) {\n const form = await request\n .formData()\n .catch(() => invalid(\"Send the message as multipart form data.\"));\n const files = form.getAll(AI_GENERATE_FIELDS.attachment);\n if (files.length > AI_LIMITS.attachmentsMax)\n invalid(\"Attach at most 4 files.\");\n if (\n files.some(\n (file) =>\n !(file instanceof Blob) ||\n (!IMAGE_TYPES.includes(file.type) && file.type !== \"application/pdf\"),\n )\n )\n invalid(\"Attach PNG, JPEG, WebP or GIF images, or PDFs.\");\n const blobs = files as File[];\n if (blobs.some((file) => file.size > AI_LIMITS.attachmentMaxBytes))\n invalid(\"Each attachment must be 5 MB or smaller.\");\n if (\n blobs.reduce((total, file) => total + file.size, 0) >\n AI_LIMITS.attachmentsTotalMaxBytes\n )\n invalid(\"Attachments must be 10 MB or smaller together.\");\n if (blobs.some((file) => file.name.length > 255))\n invalid(\"An attachment's file name must be at most 255 characters.\");\n attachments = blobs.map((file) => ({\n file,\n kind: file.type === \"application/pdf\" ? \"pdf\" : \"image\",\n name: file.name,\n }));\n const json = (field: string): unknown => {\n const raw = form.get(field);\n if (raw === null) return null;\n try {\n return JSON.parse(String(raw));\n } catch {\n return invalid(`${field} must be JSON.`);\n }\n };\n body = {\n locale: json(AI_GENERATE_FIELDS.locale),\n message: form.get(AI_GENERATE_FIELDS.message),\n selectedBlockId: form.get(AI_GENERATE_FIELDS.selectedBlockId),\n revision: form.get(AI_GENERATE_FIELDS.revision),\n mergeTags: json(AI_GENERATE_FIELDS.mergeTags),\n content: json(AI_GENERATE_FIELDS.content),\n };\n } else {\n body = await request.json().catch(() => invalid(\"Send a JSON body.\"));\n }\n if (!isRecord(body)) invalid(\"Send a JSON object.\");\n\n switch (match.route) {\n case \"altText\": {\n if (\n typeof body.blockId !== \"string\" ||\n body.blockId.length === 0 ||\n body.blockId.length > 200\n )\n invalid(\"blockId must be 1 to 200 characters.\");\n let url: URL | null = null;\n if (\n isRecord(body.image) &&\n typeof body.image.url === \"string\" &&\n body.image.url.length <= 2048\n ) {\n try {\n url = new URL(body.image.url);\n } catch {\n /* Invalid URL. */\n }\n }\n if (!url || (url.protocol !== \"http:\" && url.protocol !== \"https:\"))\n invalid(\n \"image.url must be an http: or https: URL of at most 2,048 characters.\",\n );\n for (const key of [\"currentAlt\", \"linkHref\"] as const) {\n if (\n body[key] !== null &&\n (typeof body[key] !== \"string\" || body[key].length > 2048)\n )\n invalid(\n `${key} must be a string of at most 2,048 characters or null.`,\n );\n }\n if (!isBoundedText(body.nearbyText, 2000))\n invalid(\"nearbyText must be at most 2,000 code points.\");\n if (\n typeof body.locale !== \"string\" ||\n body.locale.length > 35 ||\n !/^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$/.test(body.locale)\n )\n invalid(\"locale must be a BCP 47 tag of at most 35 characters.\");\n if (\n typeof body.maxLength !== \"number\" ||\n !Number.isInteger(body.maxLength) ||\n body.maxLength < 1 ||\n body.maxLength > 1000\n )\n invalid(\"maxLength must be an integer from 1 to 1,000.\");\n return {\n ...base,\n route: \"altText\",\n body: {\n blockId: body.blockId,\n image: { url: (body.image as { url: string }).url },\n currentAlt: body.currentAlt as string | null,\n linkHref: body.linkHref as string | null,\n nearbyText: body.nearbyText,\n locale: body.locale,\n maxLength: body.maxLength,\n },\n };\n }\n case \"settleReplay\": {\n if (\n (body.direction !== \"undo\" && body.direction !== \"redo\") ||\n typeof body.applied !== \"boolean\"\n )\n invalid(\n \"A replay outcome must have direction undo or redo and boolean applied.\",\n );\n return {\n ...base,\n route: \"settleReplay\",\n message: match.message!,\n replayId: match.replayId!,\n body: { direction: body.direction, applied: body.applied },\n };\n }\n case \"generate\": {\n const message = body.message;\n if (\n typeof message !== \"string\" ||\n message.trim() === \"\" ||\n Array.from(message).length > AI_LIMITS.messageMaxChars\n ) {\n invalid(\"The message must be 1 to 4,000 characters.\");\n }\n if (\n typeof body.selectedBlockId === \"string\" &&\n !isBoundedText(body.selectedBlockId, 200)\n )\n invalid(\"selectedBlockId must be at most 200 characters.\");\n return {\n ...base,\n route: \"generate\",\n body: {\n locale: locale(body.locale),\n message,\n attachments,\n selectedBlockId:\n typeof body.selectedBlockId === \"string\"\n ? body.selectedBlockId\n : null,\n mergeTags: mergeTags(body.mergeTags),\n content: content(body.content),\n revision: revision(body.revision),\n },\n };\n }\n case \"fixFinding\":\n return {\n ...base,\n route: \"fixFinding\",\n body: {\n locale: locale(body.locale),\n finding: finding(body.finding),\n mergeTags: mergeTags(body.mergeTags),\n content: content(body.content),\n revision: revision(body.revision),\n },\n };\n case \"rewriteText\": {\n const html = body.html;\n if (\n typeof html !== \"string\" ||\n html === \"\" ||\n Array.from(html).length > AI_LIMITS.rewriteHtmlMaxChars\n ) {\n invalid(\"html must be 1 to 20,000 characters.\");\n }\n const preset = body.preset ?? null;\n if (preset !== null && !PRESETS.includes(preset as AiRewritePreset))\n invalid(\"Unknown preset.\");\n const instruction = optionalText(\n body.instruction,\n AI_LIMITS.instructionMaxChars,\n \"The instruction must be at most 500 characters.\",\n );\n if (preset === null && instruction === null)\n invalid(\"Choose a preset or write an instruction.\");\n const language = body.language === \"\" ? null : (body.language ?? null);\n if (\n language !== null &&\n (typeof language !== \"string\" ||\n !/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/.test(language))\n ) {\n invalid(\"language must be a BCP 47 tag.\");\n }\n if (preset === \"translate\" && language === null)\n invalid(\"Choose a language to translate into.\");\n return {\n ...base,\n route: \"rewriteText\",\n body: {\n locale: locale(body.locale),\n html,\n preset: preset as AiRewritePreset | null,\n language,\n instruction,\n mergeTags: mergeTags(body.mergeTags),\n },\n };\n }\n }\n}\n\nfunction locale(value: unknown): string | null {\n if (value === undefined || value === null) return null;\n if (!isLocaleTag(value)) invalid(\"locale must be a BCP 47 tag.\");\n return value;\n}\n\n/** Validate a translation body, including direct calls to `translateFields`. */\nexport function checkTranslationRequest(value: unknown): TranslationRequest {\n if (!isRecord(value)) invalid(\"Send a JSON object.\");\n if (\n value.templateId !== null &&\n (typeof value.templateId !== \"string\" || value.templateId === \"\")\n )\n invalid(\"templateId must be a non-empty string or null.\");\n for (const key of [\"sourceLocale\", \"targetLocale\"] as const) {\n if (locale(value[key]) === null) invalid(`${key} must be a BCP 47 tag.`);\n }\n if (!Array.isArray(value.fields) || value.fields.length === 0)\n invalid(\"fields must be a non-empty array.\");\n if (value.fields.length > AI_LIMITS.translationMaxFields)\n throw new SdkError(\n AI_ERROR_MESSAGES.content_too_large,\n 413,\n \"content_too_large\",\n );\n const ids = new Set<string>();\n let chars = 0;\n for (const field of value.fields) {\n if (\n !isRecord(field) ||\n typeof field.id !== \"string\" ||\n field.id === \"\" ||\n ids.has(field.id) ||\n (field.format !== \"html\" && field.format !== \"text\") ||\n typeof field.text !== \"string\"\n )\n invalid(\n \"Each field must have a unique non-empty id, html or text format, and string text.\",\n );\n ids.add(field.id);\n chars += field.text.length;\n }\n if (chars > AI_LIMITS.translationMaxChars)\n throw new SdkError(\n AI_ERROR_MESSAGES.content_too_large,\n 413,\n \"content_too_large\",\n );\n return value as unknown as TranslationRequest;\n}\n\n/** Read the JSON translation body and enforce the translation call limits. */\nexport async function readTranslationRequest(\n request: Request,\n): Promise<TranslationRequest> {\n const body: unknown = await request\n .json()\n .catch(() => invalid(\"Send a JSON body.\"));\n return checkTranslationRequest(body);\n}\n","import type { TranslationRequest, TranslationResult } from \"@templatical/types\";\nimport { SdkError, unmaskTags } from \"@templatical/types\";\nimport { TRANSLATE_INSTRUCTIONS } from \"../generated/assets\";\nimport { stripFences } from \"./checks\";\nimport type { AiModelAdapter, AiModelMessage } from \"./model\";\nimport { fill } from \"./fill\";\nimport { languageName } from \"./prompts\";\nimport { checkTranslationRequest } from \"./read-request\";\n\nfunction passingFields(\n answer: string,\n request: TranslationRequest,\n): TranslationResult[\"fields\"] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(stripFences(answer).trim());\n } catch {\n return [];\n }\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n !Array.isArray((parsed as TranslationResult).fields)\n )\n return [];\n const fields = (parsed as { fields: unknown[] }).fields;\n return request.fields.flatMap((source) => {\n const matches = fields.filter(\n (field): field is { id: string; text?: unknown } =>\n typeof field === \"object\" &&\n field !== null &&\n \"id\" in field &&\n field.id === source.id,\n );\n if (matches.length !== 1 || typeof matches[0].text !== \"string\") return [];\n const text = matches[0].text;\n const tokens = [\n ...source.text.matchAll(\n /<x id=\"(\\d+)\"\\s*\\/>|<x id=\"(\\d+)\"><\\/x>|__x(\\d+)__/g,\n ),\n ].map(() => \"\");\n if (\n unmaskTags(source.text, tokens) === null ||\n unmaskTags(text, tokens) === null\n )\n return [];\n return [{ id: source.id, text }];\n });\n}\n\n/** Translate masked fields, with one repair and a partial result for fields that pass. */\nexport async function translateFields(options: {\n request: TranslationRequest;\n model: AiModelAdapter;\n signal?: AbortSignal;\n}): Promise<TranslationResult> {\n const { model, signal } = options;\n signal?.throwIfAborted();\n const request = checkTranslationRequest(options.request);\n const system = fill(TRANSLATE_INSTRUCTIONS, {\n source: `${languageName(request.sourceLocale)} (${request.sourceLocale})`,\n target: `${languageName(request.targetLocale)} (${request.targetLocale})`,\n });\n const messages: AiModelMessage[] = [\n {\n role: \"user\",\n content: `Translate from ${languageName(request.sourceLocale)} (${request.sourceLocale}) to ${languageName(request.targetLocale)} (${request.targetLocale}).\\n\\nFields:\\n${JSON.stringify(request.fields)}`,\n },\n ];\n const accepted = new Map<string, string>();\n for (let attempt = 0; attempt < 2; attempt++) {\n signal?.throwIfAborted();\n let answer = \"\";\n try {\n for await (const part of model.stream({\n system,\n messages,\n tools: [],\n signal,\n })) {\n signal?.throwIfAborted();\n if (part.type === \"text\") answer += part.text;\n }\n } catch (error) {\n signal?.throwIfAborted();\n if (\n (error instanceof Error && error.name === \"AbortError\") ||\n (typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n typeof error.code === \"string\" &&\n [\n \"rate_limited\",\n \"limit_reached\",\n \"content_too_large\",\n \"translation_failed\",\n ].includes(error.code))\n )\n throw error;\n if (\n !(error instanceof SdkError) ||\n (error.code !== \"ai_provider_error\" &&\n error.code !== \"ai_output_invalid\")\n )\n throw new SdkError(\n \"The translation couldn't produce a valid result. Try again.\",\n 502,\n \"translation_failed\",\n );\n }\n signal?.throwIfAborted();\n const pending = {\n ...request,\n fields: request.fields.filter((field) => !accepted.has(field.id)),\n };\n for (const field of passingFields(answer, pending))\n accepted.set(field.id, field.text);\n const failed = request.fields\n .filter((field) => !accepted.has(field.id))\n .map((field) => field.id);\n if (failed.length === 0) break;\n messages.push(\n { role: \"assistant\", content: answer },\n {\n role: \"user\",\n content: `Return complete JSON {\"fields\":[{\"id\":\"…\",\"text\":\"…\"}]} for these failed ids only: ${JSON.stringify(failed)}. Preserve every placeholder exactly once.`,\n },\n );\n }\n if (accepted.size === 0)\n throw new SdkError(\n \"The translation couldn't produce a valid result. Try again.\",\n 502,\n \"translation_failed\",\n );\n return {\n fields: request.fields.flatMap(({ id }) =>\n accepted.has(id) ? [{ id, text: accepted.get(id)! }] : [],\n ),\n };\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { t as AI_ERROR_MESSAGES } from "./wire-cYO4W7GH.js";
2
- import { i as parseRetryAfter } from "./errors-BtORdtK2.js";
1
+ import { t as AI_ERROR_MESSAGES } from "./wire-CPO4E3P9.js";
2
+ import { i as parseRetryAfter } from "./errors-BOrlo5G2.js";
3
3
  import { SdkError, ThrottledError } from "@templatical/types";
4
4
  // src/adapters/shared.ts
5
5
  /** The content an adapter stored as `{ adapter, content }` state, if it's this adapter's. */
@@ -57,4 +57,4 @@ function providerError(error) {
57
57
  }
58
58
  export { refused as a, providerError as i, cutOff as n, toolArguments as o, ownState as r, toolInput as s, blobBase64 as t };
59
59
 
60
- //# sourceMappingURL=shared-DOEVSpBX.js.map
60
+ //# sourceMappingURL=shared-Dzp1W1tL.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"shared-DOEVSpBX.js","names":[],"sources":["../src/adapters/shared.ts"],"sourcesContent":["import { SdkError, ThrottledError } from \"@templatical/types\";\nimport { parseRetryAfter } from \"../client/errors\";\nimport type { AiModelToolCall } from \"../server/model\";\nimport { AI_ERROR_MESSAGES } from \"../wire\";\n\n/** The content an adapter stored as `{ adapter, content }` state, if it's this adapter's. */\nexport function ownState<T>(state: unknown, adapter: string): T[] | undefined {\n if (typeof state !== \"object\" || state === null) return undefined;\n const tagged = state as { adapter?: unknown; content?: unknown };\n return tagged.adapter === adapter && Array.isArray(tagged.content)\n ? (tagged.content as T[])\n : undefined;\n}\n\n/** A tool call's arguments as an object (`{}` when the string isn't a JSON object). */\nexport function toolInput(\n input: AiModelToolCall[\"input\"],\n): Record<string, unknown> {\n if (typeof input !== \"string\") return input;\n try {\n const parsed: unknown = JSON.parse(input);\n return typeof parsed === \"object\" &&\n parsed !== null &&\n !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\n/** A tool call's arguments as a JSON string. */\nexport function toolArguments(input: AiModelToolCall[\"input\"]): string {\n return typeof input === \"string\" ? input : JSON.stringify(input);\n}\n\n/** A Blob's bytes as base64, with web APIs only (the kit is isomorphic). */\nexport async function blobBase64(blob: Blob): Promise<string> {\n const bytes = new Uint8Array(await blob.arrayBuffer());\n let binary = \"\";\n for (let start = 0; start < bytes.length; start += 0x8000) {\n binary += String.fromCharCode(...bytes.subarray(start, start + 0x8000));\n }\n return btoa(binary);\n}\n\n/** The model refused or a content filter stopped it. */\nexport function refused(): SdkError {\n return new SdkError(\n AI_ERROR_MESSAGES.ai_provider_error,\n 502,\n \"ai_provider_error\",\n );\n}\n\n/** The output hit the token limit while a tool call was being written. */\nexport function cutOff(): SdkError {\n return new SdkError(\n AI_ERROR_MESSAGES.ai_output_invalid,\n 422,\n \"ai_output_invalid\",\n );\n}\n\nfunction retryAfterHeader(headers: unknown): string | null {\n if (typeof Headers !== \"undefined\" && headers instanceof Headers)\n return headers.get(\"retry-after\");\n if (typeof headers !== \"object\" || headers === null) return null;\n for (const [name, value] of Object.entries(headers)) {\n if (name.toLowerCase() === \"retry-after\" && typeof value === \"string\")\n return value;\n }\n return null;\n}\n\n/**\n * A provider's 429 (after the client's own retries) as the kit's\n * `ThrottledError`; anything else unchanged, so it keeps its own details.\n * Reads the provider SDKs' `status`/`headers` and the AI SDK's\n * `statusCode`/`responseHeaders`, unwrapping a retry error's `lastError`.\n */\nexport function providerError(error: unknown): unknown {\n if (typeof error !== \"object\" || error === null) return error;\n const failure = ((error as { lastError?: unknown }).lastError ?? error) as {\n status?: unknown;\n statusCode?: unknown;\n headers?: unknown;\n responseHeaders?: unknown;\n };\n if ((failure.status ?? failure.statusCode) !== 429) return error;\n return new ThrottledError(\n AI_ERROR_MESSAGES.rate_limited,\n parseRetryAfter(\n retryAfterHeader(failure.headers ?? failure.responseHeaders),\n ),\n );\n}\n"],"mappings":";;;;;AAMA,SAAgB,SAAY,OAAgB,SAAkC;CAC5E,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO,KAAA;CACxD,MAAM,SAAS;CACf,OAAO,OAAO,YAAY,WAAW,MAAM,QAAQ,OAAO,OAAO,IAC5D,OAAO,UACR,KAAA;AACN;;AAGA,SAAgB,UACd,OACyB;CACzB,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI;EACF,MAAM,SAAkB,KAAK,MAAM,KAAK;EACxC,OAAO,OAAO,WAAW,YACvB,WAAW,QACX,CAAC,MAAM,QAAQ,MAAM,IAClB,SACD,CAAC;CACP,QAAQ;EACN,OAAO,CAAC;CACV;AACF;;AAGA,SAAgB,cAAc,OAAyC;CACrE,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK;AACjE;;AAGA,eAAsB,WAAW,MAA6B;CAC5D,MAAM,QAAQ,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC;CACrD,IAAI,SAAS;CACb,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,OACjD,UAAU,OAAO,aAAa,GAAG,MAAM,SAAS,OAAO,QAAQ,KAAM,CAAC;CAExE,OAAO,KAAK,MAAM;AACpB;;AAGA,SAAgB,UAAoB;CAClC,OAAO,IAAI,SACT,kBAAkB,mBAClB,KACA,mBACF;AACF;;AAGA,SAAgB,SAAmB;CACjC,OAAO,IAAI,SACT,kBAAkB,mBAClB,KACA,mBACF;AACF;AAEA,SAAS,iBAAiB,SAAiC;CACzD,IAAI,OAAO,YAAY,eAAe,mBAAmB,SACvD,OAAO,QAAQ,IAAI,aAAa;CAClC,IAAI,OAAO,YAAY,YAAY,YAAY,MAAM,OAAO;CAC5D,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAChD,IAAI,KAAK,YAAY,MAAM,iBAAiB,OAAO,UAAU,UAC3D,OAAO;CAEX,OAAO;AACT;;;;;;;AAQA,SAAgB,cAAc,OAAyB;CACrD,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,UAAY,MAAkC,aAAa;CAMjE,KAAK,QAAQ,UAAU,QAAQ,gBAAgB,KAAK,OAAO;CAC3D,OAAO,IAAI,eACT,kBAAkB,cAClB,gBACE,iBAAiB,QAAQ,WAAW,QAAQ,eAAe,CAC7D,CACF;AACF"}
1
+ {"version":3,"file":"shared-Dzp1W1tL.js","names":[],"sources":["../src/adapters/shared.ts"],"sourcesContent":["import { SdkError, ThrottledError } from \"@templatical/types\";\nimport { parseRetryAfter } from \"../client/errors\";\nimport type { AiModelToolCall } from \"../server/model\";\nimport { AI_ERROR_MESSAGES } from \"../wire\";\n\n/** The content an adapter stored as `{ adapter, content }` state, if it's this adapter's. */\nexport function ownState<T>(state: unknown, adapter: string): T[] | undefined {\n if (typeof state !== \"object\" || state === null) return undefined;\n const tagged = state as { adapter?: unknown; content?: unknown };\n return tagged.adapter === adapter && Array.isArray(tagged.content)\n ? (tagged.content as T[])\n : undefined;\n}\n\n/** A tool call's arguments as an object (`{}` when the string isn't a JSON object). */\nexport function toolInput(\n input: AiModelToolCall[\"input\"],\n): Record<string, unknown> {\n if (typeof input !== \"string\") return input;\n try {\n const parsed: unknown = JSON.parse(input);\n return typeof parsed === \"object\" &&\n parsed !== null &&\n !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\n/** A tool call's arguments as a JSON string. */\nexport function toolArguments(input: AiModelToolCall[\"input\"]): string {\n return typeof input === \"string\" ? input : JSON.stringify(input);\n}\n\n/** A Blob's bytes as base64, with web APIs only (the kit is isomorphic). */\nexport async function blobBase64(blob: Blob): Promise<string> {\n const bytes = new Uint8Array(await blob.arrayBuffer());\n let binary = \"\";\n for (let start = 0; start < bytes.length; start += 0x8000) {\n binary += String.fromCharCode(...bytes.subarray(start, start + 0x8000));\n }\n return btoa(binary);\n}\n\n/** The model refused or a content filter stopped it. */\nexport function refused(): SdkError {\n return new SdkError(\n AI_ERROR_MESSAGES.ai_provider_error,\n 502,\n \"ai_provider_error\",\n );\n}\n\n/** The output hit the token limit while a tool call was being written. */\nexport function cutOff(): SdkError {\n return new SdkError(\n AI_ERROR_MESSAGES.ai_output_invalid,\n 422,\n \"ai_output_invalid\",\n );\n}\n\nfunction retryAfterHeader(headers: unknown): string | null {\n if (typeof Headers !== \"undefined\" && headers instanceof Headers)\n return headers.get(\"retry-after\");\n if (typeof headers !== \"object\" || headers === null) return null;\n for (const [name, value] of Object.entries(headers)) {\n if (name.toLowerCase() === \"retry-after\" && typeof value === \"string\")\n return value;\n }\n return null;\n}\n\n/**\n * A provider's 429 (after the client's own retries) as the kit's\n * `ThrottledError`; anything else unchanged, so it keeps its own details.\n * Reads the provider SDKs' `status`/`headers` and the AI SDK's\n * `statusCode`/`responseHeaders`, unwrapping a retry error's `lastError`.\n */\nexport function providerError(error: unknown): unknown {\n if (typeof error !== \"object\" || error === null) return error;\n const failure = ((error as { lastError?: unknown }).lastError ?? error) as {\n status?: unknown;\n statusCode?: unknown;\n headers?: unknown;\n responseHeaders?: unknown;\n };\n if ((failure.status ?? failure.statusCode) !== 429) return error;\n return new ThrottledError(\n AI_ERROR_MESSAGES.rate_limited,\n parseRetryAfter(\n retryAfterHeader(failure.headers ?? failure.responseHeaders),\n ),\n );\n}\n"],"mappings":";;;;;AAMA,SAAgB,SAAY,OAAgB,SAAkC;CAC5E,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO,KAAA;CACxD,MAAM,SAAS;CACf,OAAO,OAAO,YAAY,WAAW,MAAM,QAAQ,OAAO,OAAO,IAC5D,OAAO,UACR,KAAA;AACN;;AAGA,SAAgB,UACd,OACyB;CACzB,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI;EACF,MAAM,SAAkB,KAAK,MAAM,KAAK;EACxC,OAAO,OAAO,WAAW,YACvB,WAAW,QACX,CAAC,MAAM,QAAQ,MAAM,IAClB,SACD,CAAC;CACP,QAAQ;EACN,OAAO,CAAC;CACV;AACF;;AAGA,SAAgB,cAAc,OAAyC;CACrE,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK;AACjE;;AAGA,eAAsB,WAAW,MAA6B;CAC5D,MAAM,QAAQ,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC;CACrD,IAAI,SAAS;CACb,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,OACjD,UAAU,OAAO,aAAa,GAAG,MAAM,SAAS,OAAO,QAAQ,KAAM,CAAC;CAExE,OAAO,KAAK,MAAM;AACpB;;AAGA,SAAgB,UAAoB;CAClC,OAAO,IAAI,SACT,kBAAkB,mBAClB,KACA,mBACF;AACF;;AAGA,SAAgB,SAAmB;CACjC,OAAO,IAAI,SACT,kBAAkB,mBAClB,KACA,mBACF;AACF;AAEA,SAAS,iBAAiB,SAAiC;CACzD,IAAI,OAAO,YAAY,eAAe,mBAAmB,SACvD,OAAO,QAAQ,IAAI,aAAa;CAClC,IAAI,OAAO,YAAY,YAAY,YAAY,MAAM,OAAO;CAC5D,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAChD,IAAI,KAAK,YAAY,MAAM,iBAAiB,OAAO,UAAU,UAC3D,OAAO;CAEX,OAAO;AACT;;;;;;;AAQA,SAAgB,cAAc,OAAyB;CACrD,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,UAAY,MAAkC,aAAa;CAMjE,KAAK,QAAQ,UAAU,QAAQ,gBAAgB,KAAK,OAAO;CAC3D,OAAO,IAAI,eACT,kBAAkB,cAClB,gBACE,iBAAiB,QAAQ,WAAW,QAAQ,eAAe,CAC7D,CACF;AACF"}
@@ -1,7 +1,7 @@
1
1
  import { t as AiModelAdapter } from "../model-VqqOlLS2.js";
2
2
  import { t as HttpAiFeatures } from "../http-provider-B5riig95.js";
3
- import { C as AiWireEvent } from "../wire-BJ4-EWvR.js";
4
- import { AiAltTextRequest, AiAppliedBatch, AiChatAttachment, AiChatRequest, AiFixFinding, AiFixRequest, AiMergeTags, AiOperationsResult, AiProvider, AiRewriteRequest, AiTurnHandlers, Operation, TemplateContent } from "@templatical/types";
3
+ import { C as AiWireEvent } from "../wire-JnPcAbtN.js";
4
+ import { AiAltTextRequest, AiAppliedBatch, AiChatAttachment, AiChatRequest, AiFixFinding, AiFixRequest, AiMergeTags, AiOperationsResult, AiProvider, AiRewriteRequest, AiTurnHandlers, Operation, TemplateContent, TranslationProvider } from "@templatical/types";
5
5
  interface AiConformanceCheck {
6
6
  name: string;
7
7
  ok: boolean;
@@ -12,6 +12,8 @@ interface AiConformanceReport {
12
12
  checks: AiConformanceCheck[];
13
13
  }
14
14
  interface AiConformanceOptions {
15
+ /** Active language for the probes; null for the default view. */
16
+ locale?: string | null;
15
17
  /** The template the suite edits; the test set's welcome email by default. */
16
18
  content?: TemplateContent;
17
19
  templateId?: string;
@@ -30,6 +32,8 @@ interface AiConformanceOptions {
30
32
  */
31
33
  export declare function runAiProviderConformance(provider: AiProvider, options?: AiConformanceOptions): Promise<AiConformanceReport>;
32
34
  interface AiWireConformanceOptions {
35
+ /** Active language for the probes; de by default, null for the default view. */
36
+ locale?: string | null;
33
37
  baseUrl: string;
34
38
  features?: HttpAiFeatures;
35
39
  headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
@@ -261,5 +265,18 @@ interface StandInEditor {
261
265
  };
262
266
  }
263
267
  export declare function createStandInEditor(content: TemplateContent, mergeTags?: AiMergeTags): StandInEditor;
268
+ /** Deterministic recorded translations, or a language prefix with placeholders intact. */
269
+ export declare function createMockTranslationProvider(options?: {
270
+ translations?: Record<string, Record<string, string>>;
271
+ delayMs?: number;
272
+ }): TranslationProvider;
273
+ /** Probe the translation contract without a DOM or a model-specific dependency. */
274
+ export declare function runTranslationConformance(provider: TranslationProvider, options?: {
275
+ locale?: string;
276
+ }): Promise<{
277
+ name: string;
278
+ ok: boolean;
279
+ detail?: string;
280
+ }[]>;
264
281
  export type { AiConformanceCheck, AiConformanceOptions, AiConformanceReport, AiEvalAltCheck, AiEvalAltTextInput, AiEvalCase, AiEvalCheck, AiEvalReport, AiEvalTurnInput, AiEvalTurnOutput, AiMockScript, AiRecordedEvent, AiRecordedTurn, AiRecording, AiWireConformanceOptions, StandInEditor };
265
282
  //# sourceMappingURL=index.d.ts.map
@@ -1,10 +1,10 @@
1
- import { l as aiRoutePath, o as AI_SESSION_HEADER, s as AI_WIRE_HEADER, t as AI_ERROR_MESSAGES, u as matchAiRoute } from "../wire-cYO4W7GH.js";
2
- import { n as errorFromEvent } from "../errors-BtORdtK2.js";
3
- import { n as createHttpAiProviderWithFetch } from "../http-provider-DLJYM4l8.js";
4
- import { g as buildChatRequest, h as buildAltTextRequest, i as checkAltTextAnswer, o as runEditingTurn, p as walkBlocks, r as checkAltText, t as buildAltTextRepairRequest, v as buildFixRequest } from "../checks-B-TtaywP.js";
5
- import { CollaborationConflict, OperationError, SdkError, applyOperations, createButtonBlock, createParagraphBlock, createTitleBlock, invertOperations, safeClone } from "@templatical/types";
1
+ import { l as aiRoutePath, o as AI_SESSION_HEADER, s as AI_WIRE_HEADER, t as AI_ERROR_MESSAGES, u as matchAiRoute } from "../wire-CPO4E3P9.js";
2
+ import { n as errorFromEvent } from "../errors-BOrlo5G2.js";
3
+ import { n as createHttpAiProviderWithFetch } from "../http-provider-BWVLNpZn.js";
4
+ import { _ as buildChatRequest, g as buildAltTextRequest, i as checkAltTextAnswer, m as walkBlocks, r as checkAltText, s as runEditingTurn, t as buildAltTextRepairRequest, y as buildFixRequest } from "../checks-BM_JnvRo.js";
5
+ import { CollaborationConflict, OperationError, SYNTAX_PRESETS, SdkError, applyOperations, createButtonBlock, createParagraphBlock, createTitleBlock, findOpenTagEnd, invertOperations, maskTags, resolveLocale, safeClone, unmaskTags } from "@templatical/types";
6
6
  // package.json
7
- var version = "0.52.1";
7
+ var version = "0.54.0";
8
8
  // eval/cases.json
9
9
  var cases_default = [
10
10
  {
@@ -1576,11 +1576,15 @@ async function runAiProviderConformance(provider, options = {}) {
1576
1576
  const timeoutMs = options.timeoutMs ?? 5e3;
1577
1577
  const callTimeoutMs = Math.min(options.callTimeoutMs ?? timeoutMs, timeoutMs);
1578
1578
  const templateId = options.templateId ?? "conformance";
1579
- const editor = createStandInEditor(options.content ?? AI_EVAL_TEMPLATES.welcome, options.mergeTags ?? AI_EVAL_MERGE_TAGS);
1579
+ const locale = options.locale ?? null;
1580
+ const content = safeClone(options.content ?? AI_EVAL_TEMPLATES.welcome);
1581
+ if (locale !== null && locale !== (content.settings.locale ?? "en") && !content.settings.locales?.includes(locale)) content.settings.locales = [...content.settings.locales ?? [], locale];
1582
+ const editor = createStandInEditor(locale === null ? content : resolveLocale(content, locale), options.mergeTags ?? AI_EVAL_MERGE_TAGS);
1580
1583
  const errors = [];
1581
1584
  const probes = {};
1582
1585
  const base = () => ({
1583
1586
  templateId,
1587
+ locale,
1584
1588
  content: safeClone(editor.content),
1585
1589
  revision: null,
1586
1590
  sessionId: editor.sessionId,
@@ -1817,7 +1821,7 @@ async function runAiProviderConformance(provider, options = {}) {
1817
1821
  currentAlt: null,
1818
1822
  linkHref: null,
1819
1823
  nearbyText: "Explore our email editor.",
1820
- locale: editor.content.settings.locale ?? "en",
1824
+ locale: options.locale ?? editor.content.settings.locale ?? "en",
1821
1825
  maxLength: 125
1822
1826
  };
1823
1827
  probes.altText = (signal) => altText(request, signal);
@@ -2039,6 +2043,7 @@ async function runAiWireConformance(options) {
2039
2043
  features: options.features
2040
2044
  }, observedFetch), {
2041
2045
  templateId,
2046
+ locale: options.locale === void 0 ? "de" : options.locale,
2042
2047
  altTextImageUrl: options.altTextImageUrl ?? AI_CONFORMANCE_ALT_TEXT_IMAGE_URL,
2043
2048
  ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
2044
2049
  ...options.callTimeoutMs !== void 0 ? { callTimeoutMs: options.callTimeoutMs } : {}
@@ -2425,6 +2430,134 @@ const AI_RECORDINGS = {
2425
2430
  ];
2426
2431
  }
2427
2432
  };
2428
- export { AI_CONFORMANCE_ALT_TEXT_IMAGE_URL, AI_EVAL_CASES, AI_EVAL_MERGE_TAGS, AI_EVAL_TEMPLATES, AI_RECORDINGS, checkEvalCase, createMockAiProvider, createStandInEditor, evalAltTextWith, evalTurnWith, runAiEvalSet, runAiProviderConformance, runAiWireConformance };
2433
+ // src/testing/mock-translation-provider.ts
2434
+ function prefixHtml(text, prefix) {
2435
+ let cursor = 0;
2436
+ while (cursor < text.length) if (text[cursor] === "<") {
2437
+ const end = findOpenTagEnd(text, cursor + 1);
2438
+ if (end < 0) break;
2439
+ cursor = end + 1;
2440
+ } else {
2441
+ const end = text.indexOf("<", cursor);
2442
+ if (text.slice(cursor, end < 0 ? text.length : end).trim()) return text.slice(0, cursor) + prefix + text.slice(cursor);
2443
+ cursor = end < 0 ? text.length : end;
2444
+ }
2445
+ return text + prefix;
2446
+ }
2447
+ /** Deterministic recorded translations, or a language prefix with placeholders intact. */
2448
+ function createMockTranslationProvider(options = {}) {
2449
+ return { async translate(request, signal) {
2450
+ signal.throwIfAborted();
2451
+ if ((options.delayMs ?? 0) > 0) await new Promise((resolve, reject) => {
2452
+ const stop = () => {
2453
+ clearTimeout(timer);
2454
+ reject(new DOMException("The translation was stopped.", "AbortError"));
2455
+ };
2456
+ const timer = setTimeout(() => {
2457
+ signal.removeEventListener("abort", stop);
2458
+ resolve();
2459
+ }, options.delayMs);
2460
+ signal.addEventListener("abort", stop, { once: true });
2461
+ });
2462
+ signal.throwIfAborted();
2463
+ return { fields: request.fields.map(({ id, text, format }) => ({
2464
+ id,
2465
+ text: options.translations?.[request.targetLocale]?.[id] ?? (format === "html" ? prefixHtml(text, `[${request.targetLocale}] `) : `[${request.targetLocale}] ${text}`)
2466
+ })) };
2467
+ } };
2468
+ }
2469
+ // src/testing/translation-conformance.ts
2470
+ /** Probe the translation contract without a DOM or a model-specific dependency. */
2471
+ async function runTranslationConformance(provider, options = {}) {
2472
+ const checks = [];
2473
+ const source = [{
2474
+ id: "html",
2475
+ format: "html",
2476
+ text: "<p>Hello <span data-merge-tag=\"{{name}}\">Name</span> <a href=\"{{url}}\">Shop</a></p>"
2477
+ }, {
2478
+ id: "text",
2479
+ format: "text",
2480
+ text: "Hello {{name}}"
2481
+ }];
2482
+ const masked = source.map((field) => maskTags(field.text, SYNTAX_PRESETS.liquid, field.format));
2483
+ const fields = source.map((field, i) => ({
2484
+ ...field,
2485
+ text: masked[i].text
2486
+ }));
2487
+ const request = (fields) => ({
2488
+ templateId: null,
2489
+ sourceLocale: "en",
2490
+ targetLocale: options.locale ?? "de",
2491
+ fields
2492
+ });
2493
+ const call = (fields) => withConformanceDeadline("translation", 5e3, (signal) => provider.translate(request(fields), signal));
2494
+ const shape = (result, input) => result !== null && typeof result === "object" && Array.isArray(result.fields) && result.fields.every((field) => field !== null && typeof field === "object" && typeof field.id === "string" && typeof field.text === "string" && input.some(({ id }) => id === field.id)) && new Set(result.fields.map(({ id }) => id)).size === result.fields.length;
2495
+ const add = async (name, evaluate) => {
2496
+ checks.push(await evaluateConformanceCheck(name, evaluate));
2497
+ };
2498
+ await add("placeholders round-trip", async () => {
2499
+ const result = await call(fields);
2500
+ return { ok: shape(result, fields) && result.fields.length === fields.length && fields.every((field, i) => {
2501
+ const text = result.fields.find(({ id }) => id === field.id)?.text;
2502
+ return typeof text === "string" && unmaskTags(text, masked[i].tokens) !== null;
2503
+ }) };
2504
+ });
2505
+ await add("HTML stays within the allow-list", async () => {
2506
+ const result = await call([fields[0]]);
2507
+ const allowed = /* @__PURE__ */ new Set([
2508
+ "p",
2509
+ "strong",
2510
+ "b",
2511
+ "em",
2512
+ "i",
2513
+ "u",
2514
+ "s",
2515
+ "a",
2516
+ "br",
2517
+ "span"
2518
+ ]);
2519
+ const text = result?.fields?.[0]?.text;
2520
+ if (!shape(result, [fields[0]]) || typeof text !== "string") return { ok: false };
2521
+ const clean = unmaskTags(text, masked[0].tokens.map(() => ""));
2522
+ return { ok: clean !== null && [...clean.matchAll(/<\/?([a-z][\w:-]*)\b/gi)].every((match) => allowed.has(match[1].toLowerCase())) };
2523
+ });
2524
+ await add("a partial result is accepted", async () => {
2525
+ const input = [{
2526
+ id: "partial-a",
2527
+ format: "text",
2528
+ text: "Hello"
2529
+ }, {
2530
+ id: "partial-b",
2531
+ format: "text",
2532
+ text: "Shop"
2533
+ }];
2534
+ return { ok: shape(await call(input), input) };
2535
+ });
2536
+ await add("51 fields are rejected or translated in full", async () => {
2537
+ const input = Array.from({ length: 51 }, (_, i) => ({
2538
+ id: `limit-${i}`,
2539
+ format: "text",
2540
+ text: "Hello"
2541
+ }));
2542
+ try {
2543
+ const result = await call(input);
2544
+ return { ok: shape(result, input) && result.fields.length === input.length };
2545
+ } catch (error) {
2546
+ return { ok: typeof error === "object" && error !== null && "code" in error && error.code === "content_too_large" };
2547
+ }
2548
+ });
2549
+ await add("an aborted signal rejects with AbortError", async () => {
2550
+ const controller = new AbortController();
2551
+ controller.abort();
2552
+ try {
2553
+ await withConformanceDeadline("translation abort", 5e3, (signal) => provider.translate(request(fields), signal), controller);
2554
+ return { ok: false };
2555
+ } catch (error) {
2556
+ return { ok: error instanceof Error && error.name === "AbortError" };
2557
+ }
2558
+ });
2559
+ return checks;
2560
+ }
2561
+ export { AI_CONFORMANCE_ALT_TEXT_IMAGE_URL, AI_EVAL_CASES, AI_EVAL_MERGE_TAGS, AI_EVAL_TEMPLATES, AI_RECORDINGS, checkEvalCase, createMockAiProvider, createMockTranslationProvider, createStandInEditor, evalAltTextWith, evalTurnWith, runAiEvalSet, runAiProviderConformance, runAiWireConformance, runTranslationConformance };
2429
2562
 
2430
2563
  //# sourceMappingURL=index.js.map