wizards 0.5.0 → 0.5.2

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 (160) hide show
  1. package/README.md +45 -42
  2. package/apps/runtime/dist/agents/studio-assistant.js +3 -3
  3. package/apps/runtime/dist/app.js +7 -4
  4. package/apps/runtime/dist/auth/index.js +2 -2
  5. package/apps/runtime/dist/authoring/guide.js +1 -1
  6. package/apps/runtime/dist/cli/account.js +31 -28
  7. package/apps/runtime/dist/cli/autostart.js +5 -3
  8. package/apps/runtime/dist/cli/clients.js +1 -1
  9. package/apps/runtime/dist/cli/connect.js +24 -24
  10. package/apps/runtime/dist/cli/features.js +6 -6
  11. package/apps/runtime/dist/cli/main.js +51 -39
  12. package/apps/runtime/dist/cli/mcp.js +5 -4
  13. package/apps/runtime/dist/cli/menu.js +32 -30
  14. package/apps/runtime/dist/cli/report.js +7 -9
  15. package/apps/runtime/dist/cli/setup.js +42 -38
  16. package/apps/runtime/dist/cli/start.js +16 -15
  17. package/apps/runtime/dist/cli/status.js +20 -20
  18. package/apps/runtime/dist/cli/ui.js +9 -2
  19. package/apps/runtime/dist/cli/update.js +6 -6
  20. package/apps/runtime/dist/cli/validate.js +1 -1
  21. package/apps/runtime/dist/connectors/external.js +3 -2
  22. package/apps/runtime/dist/connectors/index.js +5 -5
  23. package/apps/runtime/dist/connectors/plugin-connections.js +2 -2
  24. package/apps/runtime/dist/connectors/via-account.js +5 -5
  25. package/apps/runtime/dist/db/control-schema.js +14 -0
  26. package/apps/runtime/dist/db/migrations/control/0009_plugin_claim.sql +9 -0
  27. package/apps/runtime/dist/db/migrations/control/meta/0009_snapshot.json +561 -0
  28. package/apps/runtime/dist/db/migrations/control/meta/_journal.json +7 -0
  29. package/apps/runtime/dist/docs/dev/index.md +16 -14
  30. package/apps/runtime/dist/docs/dev/plugins/connections.md +27 -24
  31. package/apps/runtime/dist/docs/dev/plugins/events.md +19 -17
  32. package/apps/runtime/dist/docs/dev/plugins/examples.md +13 -12
  33. package/apps/runtime/dist/docs/dev/plugins/index.md +29 -24
  34. package/apps/runtime/dist/docs/dev/plugins/layout.md +20 -19
  35. package/apps/runtime/dist/docs/dev/plugins/quick-start.md +16 -15
  36. package/apps/runtime/dist/docs/dev/plugins/reference.md +52 -40
  37. package/apps/runtime/dist/docs/dev/plugins/reloading.md +24 -23
  38. package/apps/runtime/dist/docs/dev/plugins/routes.md +26 -22
  39. package/apps/runtime/dist/docs/dev/plugins/runners.md +49 -44
  40. package/apps/runtime/dist/docs/dev/plugins/server.md +95 -43
  41. package/apps/runtime/dist/docs/dev/plugins/shipping.md +19 -16
  42. package/apps/runtime/dist/docs/dev/plugins/space.md +75 -69
  43. package/apps/runtime/dist/docs/dev/plugins/starters.md +22 -17
  44. package/apps/runtime/dist/docs/dev/plugins/studio.md +80 -37
  45. package/apps/runtime/dist/docs/dev/plugins/styling.md +44 -41
  46. package/apps/runtime/dist/docs/dev/plugins/tables.md +11 -11
  47. package/apps/runtime/dist/docs/dev/plugins/tools.md +26 -25
  48. package/apps/runtime/dist/docs/dev/wizard-format.md +35 -23
  49. package/apps/runtime/dist/docs/user/ai-apps.md +46 -43
  50. package/apps/runtime/dist/docs/user/assistant.md +41 -36
  51. package/apps/runtime/dist/docs/user/build-a-wizard.md +64 -39
  52. package/apps/runtime/dist/docs/user/command-line.md +47 -44
  53. package/apps/runtime/dist/docs/user/connectors.md +40 -35
  54. package/apps/runtime/dist/docs/user/create-account.md +27 -22
  55. package/apps/runtime/dist/docs/user/data.md +29 -29
  56. package/apps/runtime/dist/docs/user/first-start.md +26 -28
  57. package/apps/runtime/dist/docs/user/index.md +16 -17
  58. package/apps/runtime/dist/docs/user/install.md +28 -27
  59. package/apps/runtime/dist/docs/user/local-only.md +21 -17
  60. package/apps/runtime/dist/docs/user/models.md +92 -84
  61. package/apps/runtime/dist/docs/user/plugins.md +28 -23
  62. package/apps/runtime/dist/docs/user/run.md +51 -50
  63. package/apps/runtime/dist/docs/user/server.md +9 -9
  64. package/apps/runtime/dist/docs/user/space.md +87 -65
  65. package/apps/runtime/dist/docs/user/steps.md +73 -69
  66. package/apps/runtime/dist/docs/user/test-and-share.md +127 -105
  67. package/apps/runtime/dist/docs/user/troubleshooting.md +40 -38
  68. package/apps/runtime/dist/docs/user/wordpress.md +62 -54
  69. package/apps/runtime/dist/documents/parse.js +2 -2
  70. package/apps/runtime/dist/engenty/shims/web-ingest.js +17 -1
  71. package/apps/runtime/dist/engine/input.js +4 -4
  72. package/apps/runtime/dist/engine/push.js +2 -2
  73. package/apps/runtime/dist/engine/runner.js +8 -8
  74. package/apps/runtime/dist/engine/steps.js +2 -2
  75. package/apps/runtime/dist/engine/surface.js +1 -1
  76. package/apps/runtime/dist/film/step.js +3 -3
  77. package/apps/runtime/dist/harness/claude.js +1 -1
  78. package/apps/runtime/dist/harness/codex.js +1 -1
  79. package/apps/runtime/dist/harness/model.js +2 -2
  80. package/apps/runtime/dist/harness/terminal.js +3 -3
  81. package/apps/runtime/dist/mcp/handler.js +2 -2
  82. package/apps/runtime/dist/mcp/tools.js +9 -9
  83. package/apps/runtime/dist/models.js +15 -15
  84. package/apps/runtime/dist/plugins/claims.js +56 -0
  85. package/apps/runtime/dist/plugins/loader.js +2 -0
  86. package/apps/runtime/dist/plugins/public.js +2 -3
  87. package/apps/runtime/dist/plugins/runs.js +13 -8
  88. package/apps/runtime/dist/render/chromium.js +24 -11
  89. package/apps/runtime/dist/routes/api.js +15 -9
  90. package/apps/runtime/dist/routes/debug.js +1 -1
  91. package/apps/runtime/dist/routes/delivery.js +46 -0
  92. package/apps/runtime/dist/routes/lang.js +7 -0
  93. package/apps/runtime/dist/routes/marketplace.js +2 -1
  94. package/apps/runtime/dist/routes/plugin-public.js +2 -1
  95. package/apps/runtime/dist/routes/plugins.js +13 -14
  96. package/apps/runtime/dist/routes/runs.js +59 -49
  97. package/apps/runtime/dist/routes/studio.js +53 -23
  98. package/apps/runtime/dist/services/access.js +3 -3
  99. package/apps/runtime/dist/services/assistant-chats.js +1 -1
  100. package/apps/runtime/dist/services/brand.js +2 -2
  101. package/apps/runtime/dist/services/cloud.js +5 -4
  102. package/apps/runtime/dist/services/errors.js +2 -1
  103. package/apps/runtime/dist/services/knowledge-model.js +2 -2
  104. package/apps/runtime/dist/services/marketplace.js +2 -2
  105. package/apps/runtime/dist/services/package-file.js +12 -9
  106. package/apps/runtime/dist/services/package.js +2 -2
  107. package/apps/runtime/dist/services/project-files.js +10 -12
  108. package/apps/runtime/dist/services/projects.js +13 -8
  109. package/apps/runtime/dist/services/runners.js +4 -3
  110. package/apps/runtime/dist/services/runs.js +3 -3
  111. package/apps/runtime/dist/services/site.js +2 -2
  112. package/apps/runtime/dist/services/space-categories.js +4 -4
  113. package/apps/runtime/dist/services/space-data.js +13 -13
  114. package/apps/runtime/dist/services/space-origin.js +1 -1
  115. package/apps/runtime/dist/services/spaces.js +7 -7
  116. package/apps/runtime/dist/services/wizards.js +5 -5
  117. package/apps/runtime/dist/tools/net-guard.js +135 -24
  118. package/apps/runtime/dist/tools/plugin.js +1 -1
  119. package/apps/runtime/dist/tools/store.js +1 -1
  120. package/apps/runtime/dist/widgets/step.js +1 -1
  121. package/apps/web/dist/assets/AboutDialog-hRgrS60N.js +1 -0
  122. package/apps/web/dist/assets/{ChangelogDialog-BXpkgatF.js → ChangelogDialog-BUSpLhC6.js} +1 -1
  123. package/apps/web/dist/assets/{ChatView-t7z44uFN.js → ChatView-DO7P5Vv_.js} +1 -1
  124. package/apps/web/dist/assets/EditorPage-BabV35Bj.js +3 -0
  125. package/apps/web/dist/assets/{FlowPreview-WpS8DCPl.js → FlowPreview-CKrwLPN0.js} +1 -1
  126. package/apps/web/dist/assets/{Harness-DL7Bse40.js → Harness-o8IIQXZp.js} +1 -1
  127. package/apps/web/dist/assets/{NewWizardChannels-Crt6PzH2.js → NewWizardChannels-DqU79Myj.js} +1 -1
  128. package/apps/web/dist/assets/{NewWizardPage-COiVmNsl.js → NewWizardPage-D1UCvmDp.js} +1 -1
  129. package/apps/web/dist/assets/{OnboardingPage-BwluEqXq.js → OnboardingPage-C70bqPnq.js} +1 -1
  130. package/apps/web/dist/assets/{ProjectPage-BIRU_AlN.js → ProjectPage-BOLvmQWZ.js} +1 -1
  131. package/apps/web/dist/assets/{PublicRunner-BxMb4gEt.js → PublicRunner-B0PGyHI9.js} +1 -1
  132. package/apps/web/dist/assets/{ReadOnly-D9r5ze5S.js → ReadOnly-B0mJBD1q.js} +1 -1
  133. package/apps/web/dist/assets/{RunDrawer-DmJl2QOJ.js → RunDrawer-BsnsIJ0Y.js} +1 -1
  134. package/apps/web/dist/assets/{RunnerView-CF_1rC-7.js → RunnerView-B56i3nKO.js} +1 -1
  135. package/apps/web/dist/assets/SettingsPage-BvfRUqb2.js +1 -0
  136. package/apps/web/dist/assets/{SetupPage-BGKtbS7j.js → SetupPage-BvAFPu1J.js} +1 -1
  137. package/apps/web/dist/assets/{SharePage-C4MqXKDA.js → SharePage-hV5pYrO2.js} +1 -1
  138. package/apps/web/dist/assets/changelog-BftXQzUA.js +1 -0
  139. package/apps/web/dist/assets/{esm-CsY8TkyE.js → esm-DfapVSy6.js} +1 -1
  140. package/apps/web/dist/assets/{index-BjQvGQp_.js → index-BuFrRzvG.js} +4 -4
  141. package/apps/web/dist/assets/{index-Bi2x2g7n.css → index-DSbQiZY8.css} +1 -1
  142. package/apps/web/dist/assets/{lock-CI6TpWL-.js → lock-B-tJwE4p.js} +1 -1
  143. package/apps/web/dist/assets/oss-credits-BJrR7BR1.js +1 -0
  144. package/apps/web/dist/assets/{phone-BrtQz-fG.js → phone-DRHxY0l_.js} +1 -1
  145. package/apps/web/dist/assets/{settings-menu-BtEKL72n.js → settings-menu-OPrJTW8a.js} +1 -1
  146. package/apps/web/dist/assets/{ui-4IaTD7LB.js → ui-DftgEA2k.js} +3 -3
  147. package/apps/web/dist/assets/{upload-CWJAtPRo.js → upload-B73coFNw.js} +1 -1
  148. package/apps/web/dist/index.html +3 -3
  149. package/apps/web/dist/mcp/flow.html +4 -4
  150. package/bin/wizards.mjs +1 -1
  151. package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +21 -2
  152. package/node_modules/@engenty-wizards/shared/dist/projects.js +1 -1
  153. package/node_modules/@engenty-wizards/shared/dist/run.d.ts +48 -48
  154. package/node_modules/@engenty-wizards/shared/dist/run.js +48 -48
  155. package/package.json +5 -4
  156. package/apps/web/dist/assets/AboutDialog-A_ILyeue.js +0 -1
  157. package/apps/web/dist/assets/EditorPage-4qxNJgKr.js +0 -3
  158. package/apps/web/dist/assets/SettingsPage-3xSInE-A.js +0 -1
  159. package/apps/web/dist/assets/changelog-CM_GdIVI.js +0 -1
  160. package/apps/web/dist/assets/oss-credits-BQQX0kvk.js +0 -1
@@ -38,11 +38,13 @@ import { sharedRun, shareImage, shareRun, shareView, unshareRun } from "../servi
38
38
  import { isIconFile, wizardAvatarSvg, wizardIcon } from "../services/wizard-icon.js";
39
39
  import { clearStore, deleteRows, listRows, listSecrets, readStoreFile, StoreError, saveRows, scopeOf, storeFiles, updateRow, } from "../store/index.js";
40
40
  import { tenantOfLink, tenantOfRun, tenantStatus, tokenOfCode, visitorOverLimit, } from "../tenants/control.js";
41
- import { byteRange, uploadLimit, wizardManifest } from "./delivery.js";
42
- import { requestLang } from "./lang.js";
41
+ import { bareMime, byteRange, inlineSafe, uploadLimit, uploadMime, wizardManifest, } from "./delivery.js";
42
+ import { inLang, notFoundAnswer, requestLang } from "./lang.js";
43
43
  const VISITOR_COOKIE = "wz_vid";
44
44
  /** Kept files a review shows inline. */
45
45
  const INLINE_TYPES = new Set(["application/pdf", "image/png", "image/jpeg", "image/webp"]);
46
+ /** Assets whose bytes and type someone outside the wizard chose: uploads and collected files. */
47
+ const FOREIGN_KINDS = new Set(["upload", "upload-image", "file"]);
46
48
  export function clientIp(c) {
47
49
  const fwd = c.req.header("x-forwarded-for")?.split(",")[0]?.trim();
48
50
  if (fwd) {
@@ -148,7 +150,7 @@ function tenantFrom(find) {
148
150
  return async (c, next) => {
149
151
  const tenant = await find(c);
150
152
  if (!tenant) {
151
- return c.json({ error: "not found" }, 404);
153
+ return c.json(notFoundAnswer(c), 404);
152
154
  }
153
155
  if ((await tenantStatus(tenant)) === "suspended") {
154
156
  return refuse(c, { code: "wizardUnavailable" }, 403);
@@ -187,7 +189,7 @@ async function pageRunner(c, rest) {
187
189
  where: eq(schema.wizard.shareToken, c.req.param("token") ?? ""),
188
190
  });
189
191
  if (!w || w.publishedVersion === null) {
190
- return c.json({ error: "not found" }, 404);
192
+ return c.json(notFoundAnswer(c), 404);
191
193
  }
192
194
  const version = await db.query.wizardVersion.findFirst({
193
195
  where: and(eq(schema.wizardVersion.wizardId, w.id), eq(schema.wizardVersion.version, w.publishedVersion)),
@@ -195,19 +197,22 @@ async function pageRunner(c, rest) {
195
197
  const served = version
196
198
  ? await servePageRunner(w, version.definition, c.req.param("runner") ?? "", rest, c.req.raw)
197
199
  : null;
198
- return served ?? c.json({ error: "not found" }, 404);
200
+ return served ?? c.json(notFoundAnswer(c), 404);
199
201
  }
200
202
  export const publicRoutes = new Hono()
201
203
  // A wizard's ID, typed into the mobile app: answers its share token.
202
204
  .get("/codes/:code", async (c) => {
203
205
  const ip = clientIp(c);
204
206
  if (codeLookupBlocked(ip)) {
205
- return c.json({ error: "too many tries" }, 429);
207
+ return c.json({
208
+ error: inLang(c, "Zu viele Versuche. Warte kurz und versuch es noch einmal.", "Too many tries. Wait a moment and try again."),
209
+ code: "too_many_tries",
210
+ }, 429);
206
211
  }
207
212
  const found = await tokenOfCode(c.req.param("code"));
208
213
  if (!found || (await tenantStatus(found.tenantId)) === "suspended") {
209
214
  codeMissed(ip);
210
- return c.json({ error: "not found" }, 404);
215
+ return c.json(notFoundAnswer(c), 404);
211
216
  }
212
217
  return c.json({ token: found.token, url: `${env.publicUrl}/w/${found.token}` });
213
218
  })
@@ -219,7 +224,7 @@ export const publicRoutes = new Hono()
219
224
  where: eq(schema.wizard.shareToken, c.req.param("token")),
220
225
  });
221
226
  if (!w || w.publishedVersion === null) {
222
- return c.json({ error: "not found" }, 404);
227
+ return c.json(notFoundAnswer(c), 404);
223
228
  }
224
229
  const version = await db.query.wizardVersion.findFirst({
225
230
  where: and(eq(schema.wizardVersion.wizardId, w.id), eq(schema.wizardVersion.version, w.publishedVersion)),
@@ -245,7 +250,7 @@ export const publicRoutes = new Hono()
245
250
  where: eq(schema.wizard.shareToken, c.req.param("token")),
246
251
  });
247
252
  if (!w || w.publishedVersion === null) {
248
- return c.json({ error: "not found" }, 404);
253
+ return c.json(notFoundAnswer(c), 404);
249
254
  }
250
255
  const reason = await wizardUnavailable(w);
251
256
  if (reason) {
@@ -372,7 +377,7 @@ export const runRoutes = new Hono()
372
377
  .get("/:id", async (c) => {
373
378
  const run = await accessibleRun(c, c.req.param("id"));
374
379
  if (!run) {
375
- return c.json({ error: "not found" }, 404);
380
+ return c.json(notFoundAnswer(c), 404);
376
381
  }
377
382
  return c.json(await viewOf(run));
378
383
  })
@@ -380,7 +385,7 @@ export const runRoutes = new Hono()
380
385
  const id = c.req.param("id");
381
386
  const first = await accessibleRun(c, id);
382
387
  if (!first) {
383
- return c.json({ error: "not found" }, 404);
388
+ return c.json(notFoundAnswer(c), 404);
384
389
  }
385
390
  return streamSSE(c, async (stream) => {
386
391
  let pending = false;
@@ -432,7 +437,7 @@ export const runRoutes = new Hono()
432
437
  .post("/:id/device", async (c) => {
433
438
  const run = await accessibleRun(c, c.req.param("id"));
434
439
  if (!run) {
435
- return c.json({ error: "not found" }, 404);
440
+ return c.json(notFoundAnswer(c), 404);
436
441
  }
437
442
  const offer = z
438
443
  .object({
@@ -451,7 +456,7 @@ export const runRoutes = new Hono()
451
456
  .post("/:id/device/:reqId", async (c) => {
452
457
  const run = await accessibleRun(c, c.req.param("id"));
453
458
  if (!run) {
454
- return c.json({ error: "not found" }, 404);
459
+ return c.json(notFoundAnswer(c), 404);
455
460
  }
456
461
  const answer = z
457
462
  .object({ text: z.string().max(200_000).optional(), error: z.string().max(500).optional() })
@@ -461,7 +466,7 @@ export const runRoutes = new Hono()
461
466
  .post("/:id/pages/:stepId", async (c) => {
462
467
  const run = await accessibleRun(c, c.req.param("id"));
463
468
  if (!run) {
464
- return c.json({ error: "not found" }, 404);
469
+ return c.json(notFoundAnswer(c), 404);
465
470
  }
466
471
  const { values } = z
467
472
  .object({ values: z.record(z.string(), z.unknown()) })
@@ -477,7 +482,7 @@ export const runRoutes = new Hono()
477
482
  .post("/:id/reviews/:stepId", async (c) => {
478
483
  const run = await accessibleRun(c, c.req.param("id"));
479
484
  if (!run) {
480
- return c.json({ error: "not found" }, 404);
485
+ return c.json(notFoundAnswer(c), 404);
481
486
  }
482
487
  const action = z
483
488
  .discriminatedUnion("type", [
@@ -501,7 +506,7 @@ export const runRoutes = new Hono()
501
506
  .post("/:id/back", async (c) => {
502
507
  const run = await accessibleRun(c, c.req.param("id"));
503
508
  if (!run) {
504
- return c.json({ error: "not found" }, 404);
509
+ return c.json(notFoundAnswer(c), 404);
505
510
  }
506
511
  try {
507
512
  await goBack(run.id);
@@ -514,7 +519,7 @@ export const runRoutes = new Hono()
514
519
  .post("/:id/retry", async (c) => {
515
520
  const run = await accessibleRun(c, c.req.param("id"));
516
521
  if (!run) {
517
- return c.json({ error: "not found" }, 404);
522
+ return c.json(notFoundAnswer(c), 404);
518
523
  }
519
524
  try {
520
525
  await retry(run.id);
@@ -527,7 +532,7 @@ export const runRoutes = new Hono()
527
532
  .post("/:id/cancel", async (c) => {
528
533
  const run = await accessibleRun(c, c.req.param("id"));
529
534
  if (!run) {
530
- return c.json({ error: "not found" }, 404);
535
+ return c.json(notFoundAnswer(c), 404);
531
536
  }
532
537
  await cancel(run.id);
533
538
  return c.json({ ok: true });
@@ -536,7 +541,7 @@ export const runRoutes = new Hono()
536
541
  .post("/:id/talk", async (c) => {
537
542
  const run = await accessibleRun(c, c.req.param("id"));
538
543
  if (!run) {
539
- return c.json({ error: "not found" }, 404);
544
+ return c.json(notFoundAnswer(c), 404);
540
545
  }
541
546
  if (run.status === "done" || run.status === "cancelled" || run.status === "failed") {
542
547
  return refuse(c, { code: "runOver" }, 409);
@@ -563,7 +568,7 @@ export const runRoutes = new Hono()
563
568
  .post("/:id/transcribe", async (c) => {
564
569
  const run = await accessibleRun(c, c.req.param("id"));
565
570
  if (!run) {
566
- return c.json({ error: "not found" }, 404);
571
+ return c.json(notFoundAnswer(c), 404);
567
572
  }
568
573
  if (run.status === "done" || run.status === "cancelled" || run.status === "failed") {
569
574
  return refuse(c, { code: "runOver" }, 409);
@@ -597,12 +602,14 @@ export const runRoutes = new Hono()
597
602
  .post("/:id/uploads", async (c) => {
598
603
  const run = await accessibleRun(c, c.req.param("id"));
599
604
  if (!run) {
600
- return c.json({ error: "not found" }, 404);
605
+ return c.json(notFoundAnswer(c), 404);
601
606
  }
602
607
  const form = await c.req.formData();
603
608
  const file = form.get("file");
604
- // A recorder names its type with codecs ("audio/webm;codecs=opus"); the bare type is kept.
605
- const mime = file instanceof File ? file.type.split(";")[0].trim().toLowerCase() : "";
609
+ // A recorder names its type with codecs ("audio/webm;codecs=opus"); the bare type is kept,
610
+ // and only when it is one a wizard takes. The browser's word is the sender's: anything else
611
+ // is kept as plain bytes, so it can never come back as a page on this origin.
612
+ const mime = uploadMime(file instanceof File ? file.type : "");
606
613
  const limit = uploadLimit(mime);
607
614
  if (!(file instanceof File) || file.size > limit) {
608
615
  return refuse(c, { code: "fileTooLarge", params: { mb: Math.round(limit / 1_000_000) } }, 400);
@@ -610,7 +617,7 @@ export const runRoutes = new Hono()
610
617
  const ref = await saveAsset({
611
618
  runId: run.id,
612
619
  kind: mime.startsWith("image/") ? "upload-image" : "upload",
613
- mime: mime || "application/octet-stream",
620
+ mime,
614
621
  name: file.name.slice(0, 120),
615
622
  data: new Uint8Array(await file.arrayBuffer()),
616
623
  });
@@ -620,7 +627,7 @@ export const runRoutes = new Hono()
620
627
  .post("/:id/geocode", async (c) => {
621
628
  const run = await accessibleRun(c, c.req.param("id"));
622
629
  if (!run) {
623
- return c.json({ error: "not found" }, 404);
630
+ return c.json(notFoundAnswer(c), 404);
624
631
  }
625
632
  const { lat, lng, language } = z
626
633
  .object({
@@ -649,7 +656,7 @@ export const runRoutes = new Hono()
649
656
  .post("/:id/ask/:askId", async (c) => {
650
657
  const run = await accessibleRun(c, c.req.param("id"));
651
658
  if (!run) {
652
- return c.json({ error: "not found" }, 404);
659
+ return c.json(notFoundAnswer(c), 404);
653
660
  }
654
661
  const answer = z
655
662
  .discriminatedUnion("type", [
@@ -673,7 +680,7 @@ export const runRoutes = new Hono()
673
680
  const resources = run?.status === "running" ? liveResources(run.id) : null;
674
681
  if (!resources?.openPage()) {
675
682
  // The run may work in another process: its runner answers.
676
- return (run && (await fromOwner(c, run.id))) || c.json({ error: "not found" }, 404);
683
+ return (run && (await fromOwner(c, run.id))) || c.json(notFoundAnswer(c), 404);
677
684
  }
678
685
  return c.json({ seat: resources.seatHolder() });
679
686
  })
@@ -831,7 +838,7 @@ export const runRoutes = new Hono()
831
838
  const run = await accessibleRun(c, c.req.param("id"));
832
839
  const connection = run?.definition.connections?.find((x) => x.id === c.req.param("connectionId"));
833
840
  if (!run || !connection) {
834
- return c.json({ error: "not found" }, 404);
841
+ return c.json(notFoundAnswer(c), 404);
835
842
  }
836
843
  try {
837
844
  const url = await startOAuth(scopeOf(run), await projectIdOf(run), run.id, connection, c.req.param("connectorId"), requestLang(c));
@@ -848,7 +855,7 @@ export const runRoutes = new Hono()
848
855
  const run = await accessibleRun(c, c.req.param("id"));
849
856
  const connection = run?.definition.connections?.find((x) => x.id === c.req.param("connectionId"));
850
857
  if (!run || !connection) {
851
- return c.json({ error: "not found" }, 404);
858
+ return c.json(notFoundAnswer(c), 404);
852
859
  }
853
860
  const { values } = z
854
861
  .object({ values: z.record(z.string(), z.string().max(500)) })
@@ -868,7 +875,7 @@ export const runRoutes = new Hono()
868
875
  .delete("/:id/connections/:connectionId", async (c) => {
869
876
  const run = await accessibleRun(c, c.req.param("id"));
870
877
  if (!run) {
871
- return c.json({ error: "not found" }, 404);
878
+ return c.json(notFoundAnswer(c), 404);
872
879
  }
873
880
  await disconnect(scopeOf(run), c.req.param("connectionId"));
874
881
  signalChanged(run.id);
@@ -880,7 +887,7 @@ export const runRoutes = new Hono()
880
887
  // A shared list is every run's: the person never sees it.
881
888
  const def = run?.definition.lists?.find((l) => l.id === c.req.param("list") && !l.shared);
882
889
  if (!run || !def) {
883
- return c.json({ error: "not found" }, 404);
890
+ return c.json(notFoundAnswer(c), 404);
884
891
  }
885
892
  const { cells } = z
886
893
  .object({ cells: z.record(z.string(), z.unknown()) })
@@ -899,14 +906,14 @@ export const runRoutes = new Hono()
899
906
  // A shared list is every run's: the person never sees it.
900
907
  const def = run?.definition.lists?.find((l) => l.id === c.req.param("list") && !l.shared);
901
908
  if (!run || !def) {
902
- return c.json({ error: "not found" }, 404);
909
+ return c.json(notFoundAnswer(c), 404);
903
910
  }
904
911
  const { cells } = z
905
912
  .object({ cells: z.record(z.string(), z.unknown()) })
906
913
  .parse(await c.req.json());
907
914
  try {
908
915
  if (!(await updateRow(scopeOf(run), def, c.req.param("rowId"), cells))) {
909
- return c.json({ error: "not found" }, 404);
916
+ return c.json(notFoundAnswer(c), 404);
910
917
  }
911
918
  }
912
919
  catch (err) {
@@ -920,7 +927,7 @@ export const runRoutes = new Hono()
920
927
  // A shared list is every run's: the person never sees it.
921
928
  const def = run?.definition.lists?.find((l) => l.id === c.req.param("list") && !l.shared);
922
929
  if (!run || !def) {
923
- return c.json({ error: "not found" }, 404);
930
+ return c.json(notFoundAnswer(c), 404);
924
931
  }
925
932
  await deleteRows(scopeOf(run), def, [c.req.param("rowId")]);
926
933
  signalChanged(run.id);
@@ -944,7 +951,7 @@ export const runRoutes = new Hono()
944
951
  .get("/:id/store", async (c) => {
945
952
  const run = await accessibleRun(c, c.req.param("id"));
946
953
  if (!run) {
947
- return c.json({ error: "not found" }, 404);
954
+ return c.json(notFoundAnswer(c), 404);
948
955
  }
949
956
  const scope = scopeOf(run);
950
957
  const lists = await Promise.all((run.definition.lists ?? [])
@@ -989,7 +996,7 @@ export const runRoutes = new Hono()
989
996
  .delete("/:id/store", async (c) => {
990
997
  const run = await accessibleRun(c, c.req.param("id"));
991
998
  if (!run) {
992
- return c.json({ error: "not found" }, 404);
999
+ return c.json(notFoundAnswer(c), 404);
993
1000
  }
994
1001
  await clearStore(scopeOf(run));
995
1002
  signalChanged(run.id);
@@ -999,7 +1006,7 @@ export const runRoutes = new Hono()
999
1006
  .post("/:id/notify", async (c) => {
1000
1007
  const run = await accessibleRun(c, c.req.param("id"));
1001
1008
  if (!run) {
1002
- return c.json({ error: "not found" }, 404);
1009
+ return c.json(notFoundAnswer(c), 404);
1003
1010
  }
1004
1011
  const body = z
1005
1012
  .object({
@@ -1014,7 +1021,7 @@ export const runRoutes = new Hono()
1014
1021
  .delete("/:id/notify", async (c) => {
1015
1022
  const run = await accessibleRun(c, c.req.param("id"));
1016
1023
  if (!run) {
1017
- return c.json({ error: "not found" }, 404);
1024
+ return c.json(notFoundAnswer(c), 404);
1018
1025
  }
1019
1026
  await setPushDevice(run.id, null);
1020
1027
  return c.json({ ok: true });
@@ -1022,7 +1029,7 @@ export const runRoutes = new Hono()
1022
1029
  .post("/:id/share", async (c) => {
1023
1030
  const run = await accessibleRun(c, c.req.param("id"));
1024
1031
  if (!run) {
1025
- return c.json({ error: "not found" }, 404);
1032
+ return c.json(notFoundAnswer(c), 404);
1026
1033
  }
1027
1034
  try {
1028
1035
  return c.json(await shareRun(run));
@@ -1037,7 +1044,7 @@ export const runRoutes = new Hono()
1037
1044
  .delete("/:id/share", async (c) => {
1038
1045
  const run = await accessibleRun(c, c.req.param("id"));
1039
1046
  if (!run) {
1040
- return c.json({ error: "not found" }, 404);
1047
+ return c.json(notFoundAnswer(c), 404);
1041
1048
  }
1042
1049
  await unshareRun(run);
1043
1050
  return c.json({ ok: true });
@@ -1108,22 +1115,25 @@ async function serveAsset(c, run, assetId) {
1108
1115
  if (!found || found.row.runId !== run.id) {
1109
1116
  return c.notFound();
1110
1117
  }
1118
+ // The stored type is sent only as a bare `type/subtype`: "text/html,x" reads as HTML to a browser.
1119
+ const mime = bareMime(found.row.mime) ?? "application/octet-stream";
1120
+ // What a person or a plugin uploaded, and files a step collected from mail and the web.
1121
+ const foreign = FOREIGN_KINDS.has(found.row.kind);
1111
1122
  const headers = {
1112
- "content-type": found.row.mime,
1123
+ "content-type": mime,
1113
1124
  "cache-control": "private, max-age=3600",
1114
1125
  "x-content-type-options": "nosniff",
1126
+ // Nothing opened on its own runs scripts with this origin (an SVG, an XML document, a page).
1127
+ // A PDF is left to the browser's own viewer, which a sandbox would keep from starting.
1128
+ ...(mime === "application/pdf" ? {} : { "content-security-policy": "sandbox" }),
1115
1129
  };
1116
- // Files a step collected come from mail and the web: they are downloads, never pages of ours.
1117
- if (found.row.kind === "file") {
1130
+ // Foreign files are downloads, never pages of ours: only photos, sound, film and PDFs show in place.
1131
+ if (foreign && !inlineSafe(mime)) {
1118
1132
  headers["content-disposition"] =
1119
1133
  `attachment; filename="${found.row.name.replace(/["\\\r\n]/g, "")}"`;
1120
1134
  }
1121
- // An SVG or XML document opened on its own could run scripts with this origin.
1122
- if (/svg|xml/.test(found.row.mime)) {
1123
- headers["content-security-policy"] = "sandbox";
1124
- }
1125
1135
  // Generated HTML runs its scripts in an opaque origin and can reach nothing.
1126
- if (found.row.mime === "text/html") {
1136
+ if (mime === "text/html" && !foreign) {
1127
1137
  const html = await stepHtml({
1128
1138
  assets: [{ id: assetId, kind: found.row.kind, mime: found.row.mime, name: "" }],
1129
1139
  at: "",
@@ -1132,7 +1142,7 @@ async function serveAsset(c, run, assetId) {
1132
1142
  headers["content-security-policy"] = HTML_RESPONSE_CSP;
1133
1143
  return c.body(new TextEncoder().encode(html ?? ""), 200, headers);
1134
1144
  }
1135
- if (/^(audio|video)\//.test(found.row.mime)) {
1145
+ if (/^(audio|video)\//.test(mime)) {
1136
1146
  headers["accept-ranges"] = "bytes";
1137
1147
  const size = found.data.byteLength;
1138
1148
  const range = byteRange(c.req.header("range"), size);
@@ -281,7 +281,10 @@ export const studio = new Hono()
281
281
  // The team as the Manage-App keeps it: its people, who is invited, the places the plan gives.
282
282
  .get("/team", async (c) => {
283
283
  if (!managed) {
284
- return c.json({ error: "not_managed" }, 404);
284
+ return c.json({
285
+ error: inLang(c, "Das gibt es nur in der Cloud.", "This is only available in the cloud."),
286
+ code: "not_managed",
287
+ }, 404);
285
288
  }
286
289
  const user = c.get("user");
287
290
  const [info, team] = await Promise.all([
@@ -300,11 +303,17 @@ export const studio = new Hono()
300
303
  // A runtime that runs alone says when a newer release is out and can run the update itself;
301
304
  // `?fresh=1` asks GitHub now instead of within the hour.
302
305
  .get("/update", async (c) => managed
303
- ? c.json({ error: "not_local" }, 404)
306
+ ? c.json({
307
+ error: inLang(c, "Das gibt es nur auf deinem Computer.", "This is only available on your computer."),
308
+ code: "not_local",
309
+ }, 404)
304
310
  : c.json(await updateStatus(c.req.query("fresh") === "1")))
305
311
  .post("/update", async (c) => {
306
312
  if (managed) {
307
- return c.json({ error: "not_local" }, 404);
313
+ return c.json({
314
+ error: inLang(c, "Das gibt es nur auf deinem Computer.", "This is only available on your computer."),
315
+ code: "not_local",
316
+ }, 404);
308
317
  }
309
318
  applyUpdate();
310
319
  return c.json(await updateStatus());
@@ -333,7 +342,7 @@ export const studio = new Hono()
333
342
  .post("/assistant/speak", async (c) => {
334
343
  const { text } = z.object({ text: z.string().min(1).max(4000) }).parse(await c.req.json());
335
344
  if ((await classWays()).speech.kind === "none") {
336
- throw new ServiceError("refused", bilingual("Hier spricht kein Sprachmodell.", "No language model speaks here."));
345
+ throw new ServiceError("refused", bilingual("Hier ist kein Sprachmodell eingerichtet.", "No language model is set up here."));
337
346
  }
338
347
  if (!(await canSpend())) {
339
348
  throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
@@ -472,8 +481,8 @@ export const studio = new Hono()
472
481
  message: err instanceof ServiceError || err instanceof ModelUnavailableError
473
482
  ? errorText(err, body.lang)
474
483
  : body.lang === "de"
475
- ? "Da ist etwas schiefgelaufen. Bitte noch einmal versuchen."
476
- : "Something went wrong. Please try again.",
484
+ ? "Da ist etwas schiefgelaufen. Versuch es noch einmal."
485
+ : "Something went wrong. Try again.",
477
486
  });
478
487
  }
479
488
  endRun(runId);
@@ -483,7 +492,7 @@ export const studio = new Hono()
483
492
  })
484
493
  // --- projects --------------------------------------------------------------
485
494
  .get("/projects", async (c) => {
486
- const projects = await listProjects(c.get("user").id);
495
+ const projects = await listProjects(c.get("user").id, requestLang(c));
487
496
  const build = await mayBuild();
488
497
  return c.json(projects.map((p) => ({
489
498
  id: p.id,
@@ -521,7 +530,7 @@ export const studio = new Hono()
521
530
  const kind = z.enum(PROJECT_FILE_KINDS).parse(c.req.query("kind"));
522
531
  const file = (await c.req.formData()).get("file");
523
532
  if (!(file instanceof File)) {
524
- throw new ServiceError("invalid", bilingual("Bitte eine Datei wählen.", "Please choose a file."));
533
+ throw new ServiceError("invalid", bilingual("Wähl eine Datei.", "Choose a file."));
525
534
  }
526
535
  const row = await addProjectFile(project.id, {
527
536
  kind,
@@ -554,7 +563,7 @@ export const studio = new Hono()
554
563
  await ownedProject(c.get("user").id, c.req.param("id"));
555
564
  const { url } = z.object({ url: z.string().min(1).max(2000) }).parse(await c.req.json());
556
565
  const colors = await scanColors(url).catch(() => {
557
- throw new ServiceError("invalid", bilingual("Die Seite ist nicht erreichbar.", "The page cannot be reached."));
566
+ throw new ServiceError("invalid", bilingual("Die Seite ist nicht erreichbar.", "The page can’t be reached."));
558
567
  });
559
568
  return c.json({ colors });
560
569
  })
@@ -588,7 +597,7 @@ export const studio = new Hono()
588
597
  await ownedProject(c.get("user").id, c.req.param("id"));
589
598
  const { url } = z.object({ url: z.string().min(1).max(2000) }).parse(await c.req.json());
590
599
  const fonts = await scanFonts(url).catch(() => {
591
- throw new ServiceError("invalid", bilingual("Die Seite ist nicht erreichbar.", "The page cannot be reached."));
600
+ throw new ServiceError("invalid", bilingual("Die Seite ist nicht erreichbar.", "The page can’t be reached."));
592
601
  });
593
602
  return c.json({ fonts });
594
603
  })
@@ -830,7 +839,7 @@ export const studio = new Hono()
830
839
  ? { fileId: id }
831
840
  : null;
832
841
  if (!ref || key[1] !== ":") {
833
- throw new ServiceError("invalid", "Unbekannter Eintrag.");
842
+ throw new ServiceError("invalid", bilingual("Unbekannter Eintrag.", "Unknown entry."));
834
843
  }
835
844
  const { values } = z
836
845
  .object({ values: z.array(z.union([z.string().max(300), z.number(), z.boolean()])).max(50) })
@@ -944,7 +953,7 @@ export const studio = new Hono()
944
953
  .post("/projects/:id/wizards/import", async (c) => {
945
954
  const file = (await c.req.formData()).get("file");
946
955
  if (!(file instanceof File) || !file.size || file.size > PACKAGE_MAX_BYTES) {
947
- throw new ServiceError("invalid", bilingual(`Bitte ein Wizard-Paket (${PACKAGE_EXTENSION}) bis 30 MB.`, `Please a wizard package (${PACKAGE_EXTENSION}) of up to 30 MB.`));
956
+ throw new ServiceError("invalid", bilingual(`Wähl eine Wizard-Datei (${PACKAGE_EXTENSION}) bis 30 MB.`, `Choose a wizard file (${PACKAGE_EXTENSION}) of 30 MB or less.`));
948
957
  }
949
958
  return c.json(await importWizard(c.get("user").id, c.req.param("id"), new Uint8Array(await file.arrayBuffer()), file.name, requestLang(c)));
950
959
  })
@@ -1137,7 +1146,7 @@ export const studio = new Hono()
1137
1146
  data: JSON.stringify({
1138
1147
  message: err instanceof ServiceError
1139
1148
  ? errorText(err, requestLang(c))
1140
- : inLang(c, "Da ist etwas schiefgelaufen. Bitte noch einmal versuchen.", "Something went wrong. Please try again."),
1149
+ : inLang(c, "Da ist etwas schiefgelaufen. Versuch es noch einmal.", "Something went wrong. Try again."),
1141
1150
  }),
1142
1151
  });
1143
1152
  }
@@ -1147,7 +1156,7 @@ export const studio = new Hono()
1147
1156
  .post("/transcribe", async (c) => {
1148
1157
  const file = (await c.req.formData()).get("file");
1149
1158
  if (!(file instanceof File) || !file.size || file.size > 10_000_000) {
1150
- throw new ServiceError("invalid", bilingual("Bitte eine Aufnahme bis 10 MB.", "Please a recording of up to 10 MB."));
1159
+ throw new ServiceError("invalid", bilingual("Die Aufnahme darf höchstens 10 MB groß sein.", "The recording can be 10 MB at most."));
1151
1160
  }
1152
1161
  if (!(await canSpend())) {
1153
1162
  throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
@@ -1221,7 +1230,7 @@ export const studio = new Hono()
1221
1230
  const { connect } = z.object({ connect: z.boolean() }).parse(await c.req.json());
1222
1231
  const [done] = await connectApps(layout(), [c.req.param("id")], !connect);
1223
1232
  if (!done) {
1224
- return c.json({ error: "Unknown app" }, 404);
1233
+ return c.json({ error: inLang(c, "Diese App gibt es nicht.", "Unknown app."), code: "not_found" }, 404);
1225
1234
  }
1226
1235
  // Not done is an answer too: the app's file is no plain JSON, here is what to paste.
1227
1236
  return c.json(done.outcome);
@@ -1241,7 +1250,10 @@ export const studio = new Hono()
1241
1250
  .get("/models", async (c) => c.json(await modelSettings(requestLang(c))))
1242
1251
  .put("/models", async (c) => {
1243
1252
  if (!mayChangeModels(c.get("user"))) {
1244
- return c.json({ error: "Only an admin of the team changes its models." }, 403);
1253
+ return c.json({
1254
+ error: inLang(c, "Nur Admins des Teams ändern seine Modelle.", "Only team admins can change its models."),
1255
+ code: "forbidden",
1256
+ }, 403);
1245
1257
  }
1246
1258
  const input = modelsInput.parse(await c.req.json());
1247
1259
  // In the cloud nothing runs on a machine's client or local model.
@@ -1249,7 +1261,10 @@ export const studio = new Hono()
1249
1261
  (input.source ||
1250
1262
  input.ollamaUrl ||
1251
1263
  Object.values(input.bindings ?? {}).some((ref) => ref && !cloudBinding(ref)))) {
1252
- return c.json({ error: "Not available in the cloud." }, 400);
1264
+ return c.json({
1265
+ error: inLang(c, "Das gibt es nicht in der Cloud.", "This isn’t available in the cloud."),
1266
+ code: "not_local",
1267
+ }, 400);
1253
1268
  }
1254
1269
  // Own keys and bindings to a provider's model are a feature of the team's plan.
1255
1270
  if (managed) {
@@ -1258,7 +1273,7 @@ export const studio = new Hono()
1258
1273
  Object.values(input.bindings ?? {}).some((ref) => ref && !isCreditsRef(ref));
1259
1274
  if (own && info && !feature("ownKeys", info.features)) {
1260
1275
  return c.json({
1261
- error: inLang(c, `Eigene API Keys gibt es nicht im Paket ${info.plan?.name ?? "des Teams"}.`, `Own API keys are not part of the ${info.plan?.name ?? "team's"} plan.`),
1276
+ error: inLang(c, `Eigene API-Keys gibt es nicht im Paket ${info.plan?.name ?? "des Teams"}.`, `Your own API keys aren’t part of the ${info.plan?.name ?? "team’s"} plan.`),
1262
1277
  code: "plan",
1263
1278
  }, 403);
1264
1279
  }
@@ -1274,7 +1289,10 @@ export const studio = new Hono()
1274
1289
  .parse(await c.req.json().catch(() => ({})));
1275
1290
  const value = key?.trim() || (await storedKey(provider));
1276
1291
  if (!value) {
1277
- return c.json({ ok: false, message: "Kein API Key hinterlegt." });
1292
+ return c.json({
1293
+ ok: false,
1294
+ message: inLang(c, "Kein API-Key hinterlegt.", "No API key is set."),
1295
+ });
1278
1296
  }
1279
1297
  return c.json(await checkKey(provider, value, requestLang(c)));
1280
1298
  })
@@ -1282,7 +1300,10 @@ export const studio = new Hono()
1282
1300
  // clip comes back to look at, a voice note comes back as text. Paid like any call.
1283
1301
  .post("/models/test", async (c) => {
1284
1302
  if (!mayChangeModels(c.get("user"))) {
1285
- return c.json({ error: "Only an admin of the team tests its models." }, 403);
1303
+ return c.json({
1304
+ error: inLang(c, "Nur Admins des Teams testen seine Modelle.", "Only team admins can test its models."),
1305
+ code: "forbidden",
1306
+ }, 403);
1286
1307
  }
1287
1308
  const { cls, lang } = z
1288
1309
  .object({ cls: z.enum(MODEL_CLASSES), lang: z.enum(["de", "en"]).default("en") })
@@ -1439,7 +1460,10 @@ export const studio = new Hono()
1439
1460
  // --- the first visit of a cloud studio: who the team is, its website, first documents -----
1440
1461
  .put("/team", async (c) => {
1441
1462
  if (!managed) {
1442
- return c.json({ error: "not_managed" }, 404);
1463
+ return c.json({
1464
+ error: inLang(c, "Das gibt es nur in der Cloud.", "This is only available in the cloud."),
1465
+ code: "not_managed",
1466
+ }, 404);
1443
1467
  }
1444
1468
  await requireAdmin();
1445
1469
  const { name } = z.object({ name: z.string().trim().min(1).max(80) }).parse(await c.req.json());
@@ -1448,7 +1472,10 @@ export const studio = new Hono()
1448
1472
  })
1449
1473
  .post("/onboarding/done", async (c) => {
1450
1474
  if (!managed) {
1451
- return c.json({ error: "not_managed" }, 404);
1475
+ return c.json({
1476
+ error: inLang(c, "Das gibt es nur in der Cloud.", "This is only available in the cloud."),
1477
+ code: "not_managed",
1478
+ }, 404);
1452
1479
  }
1453
1480
  await finishOnboarding(c.get("user").tenantId);
1454
1481
  return c.json({ ok: true });
@@ -1462,7 +1489,10 @@ export const studio = new Hono()
1462
1489
  // The install command and the first steps, mailed to the signed-in person by the Manage-App.
1463
1490
  .post("/install-mail", async (c) => {
1464
1491
  if (!managed) {
1465
- return c.json({ error: "not_managed" }, 404);
1492
+ return c.json({
1493
+ error: inLang(c, "Das gibt es nur in der Cloud.", "This is only available in the cloud."),
1494
+ code: "not_managed",
1495
+ }, 404);
1466
1496
  }
1467
1497
  const { lang } = z
1468
1498
  .object({ lang: z.enum(["de", "en"]).default("en") })
@@ -31,7 +31,7 @@ export function currentRole() {
31
31
  return actor.getStore() ?? "owner";
32
32
  }
33
33
  export const isAdminRole = (role) => role === "owner" || role === "admin";
34
- export const MEMBERS_EDIT_ONLY = bilingual("Das dürfen nur Admins des Teams. Als Mitglied bearbeitest du die Wizards, die es gibt.", "Only the team's admins may do that. As a member you edit the wizards that are there.");
34
+ export const MEMBERS_EDIT_ONLY = bilingual("Das dürfen nur Admins des Teams. Als Mitglied bearbeitest du die Wizards, die es schon gibt.", "Only team admins can do that. As a member, you can edit the wizards that already exist.");
35
35
  /** Refuses a member: making and deleting is for the owner and the admins of the team. */
36
36
  export async function requireAdmin() {
37
37
  if (syncing() || !managed || isAdminRole(currentRole())) {
@@ -52,8 +52,8 @@ export async function mayBuild() {
52
52
  // An older Manage-App does not say: then it builds. One that cannot be reached: nothing changes.
53
53
  return info ? info.limits.build !== false : false;
54
54
  }
55
- export const READ_ONLY_PROJECT = bilingual("Dieses Projekt kommt aus einer lokalen Installation und wird dort geändert.", "This project comes from a local install and is changed there.");
56
- export const READ_ONLY_TENANT = bilingual("Hier wird nichts gebaut: Auf diesem Server laufen die Wizards, die deine lokale Installation veröffentlicht.", "Nothing is built here: this server runs the wizards your local install publishes.");
55
+ export const READ_ONLY_PROJECT = bilingual("Diesen Space bearbeitest du in engenty wizards auf deinem Computer, nicht hier.", "You edit this space in engenty wizards on your computer, not here.");
56
+ export const READ_ONLY_TENANT = bilingual("Hier laufen die Wizards, die du in engenty wizards auf deinem Computer veröffentlichst. Gebaut und bearbeitet wird dort.", "Wizards run here once you publish them in engenty wizards on your computer. You build and edit them there.");
57
57
  /** Refuses when the tenant builds nothing here. */
58
58
  export async function requireBuild() {
59
59
  if (syncing() || (await mayBuild())) {
@@ -253,7 +253,7 @@ export async function readChat(user, chatId) {
253
253
  where: and(mine(user), eq(schema.assistantChat.id, chatId)),
254
254
  });
255
255
  if (!chat) {
256
- throw new ServiceError("not_found", bilingual("Diesen Chat gibt es nicht.", "There is no such chat."));
256
+ throw new ServiceError("not_found", bilingual("Diesen Chat gibt es nicht.", "This chat doesn’t exist."));
257
257
  }
258
258
  return {
259
259
  id: chat.id,
@@ -66,14 +66,14 @@ export async function describeGradient(brand, text, current) {
66
66
  answer = result.output;
67
67
  }
68
68
  catch {
69
- throw new ServiceError("invalid", bilingual("Daraus ließ sich kein Verlauf machen. Beschreib ihn bitte anders.", "No gradient could be made from that. Please describe it differently."));
69
+ throw new ServiceError("invalid", bilingual("Daraus ließ sich kein Verlauf machen. Beschreib ihn anders.", "Couldn’t make a gradient from that. Try describing it differently."));
70
70
  }
71
71
  const stops = answer.stops
72
72
  .filter((s) => HEX.test(s.color))
73
73
  .slice(0, 12)
74
74
  .map((s) => ({ color: s.color.toLowerCase(), at: Math.min(100, Math.max(0, s.at)) }));
75
75
  if (stops.length < 2) {
76
- throw new ServiceError("invalid", bilingual("Daraus ließ sich kein Verlauf machen. Beschreib ihn bitte anders.", "No gradient could be made from that. Please describe it differently."));
76
+ throw new ServiceError("invalid", bilingual("Daraus ließ sich kein Verlauf machen. Beschreib ihn anders.", "Couldn’t make a gradient from that. Try describing it differently."));
77
77
  }
78
78
  return {
79
79
  name: answer.name.trim().slice(0, 80),