@xopcai/xopc 0.0.222 → 0.0.224

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 (213) hide show
  1. package/dist/browser-ext/manifest.json +1 -1
  2. package/dist/extensions/feishu/src/outbound/media-load.js +1 -1
  3. package/dist/extensions/telegram/xopc.extension.json +1 -1
  4. package/dist/gateway/static/root/assets/{activity-store-NItGQmvR.js → activity-store--GBciH10.js} +1 -1
  5. package/dist/gateway/static/root/assets/{agent-avatar-dicebear-DsR_2U9-.js → agent-avatar-dicebear-K7V4XXt7.js} +1 -1
  6. package/dist/gateway/static/root/assets/agents-BsxTpXyq.js +68 -0
  7. package/dist/gateway/static/root/assets/{api-53ZXsuyr.js → api-HoIz_ija.js} +1 -1
  8. package/dist/gateway/static/root/assets/{app-management-settings-panel-BCzVX09V.js → app-management-settings-panel-xQCmkAHh.js} +1 -1
  9. package/dist/gateway/static/root/assets/{appearance-settings-DgSpwqQI.js → appearance-settings-BRoeA4kU.js} +1 -1
  10. package/dist/gateway/static/root/assets/apps-page-BXgpBA8w.js +1 -0
  11. package/dist/gateway/static/root/assets/{archive-plugin-D8o8CP9Y.js → archive-plugin-CteCVsBP.js} +1 -1
  12. package/dist/gateway/static/root/assets/{attachment-load-BQHcAR6J.js → attachment-load-D7xNA2jB.js} +1 -1
  13. package/dist/gateway/static/root/assets/{automations-page-CswK3HFb.js → automations-page-BD1vK7r5.js} +1 -1
  14. package/dist/gateway/static/root/assets/automations-workspace-wrgf-z7D.js +6 -0
  15. package/dist/gateway/static/root/assets/{binary-plugins-BlpyTJ1b.js → binary-plugins-C3DaJa0W.js} +1 -1
  16. package/dist/gateway/static/root/assets/block-editor-lTkcHTve.js +184 -0
  17. package/dist/gateway/static/root/assets/browser-settings-page-zhzNkeVG.js +8 -0
  18. package/dist/gateway/static/root/assets/{browser-workflow-inputs-Bi6RSSpD.js → browser-workflow-inputs-1UZ0vnIy.js} +1 -1
  19. package/dist/gateway/static/root/assets/{browser-workflows-page-CZNOOQY4.js → browser-workflows-page-BHuJOF4M.js} +2 -2
  20. package/dist/gateway/static/root/assets/capabilities-settings-panel-DuCmg2Ej.js +7 -0
  21. package/dist/gateway/static/root/assets/{capability-presets-api-pC2fno7r.js → capability-presets-api-yNqTIuyd.js} +1 -1
  22. package/dist/gateway/static/root/assets/capability-presets-settings-panel-DjiP3mQQ.js +3 -0
  23. package/dist/gateway/static/root/assets/{channel-recipient-api-B5Szgu_z.js → channel-recipient-api-krezGsKc.js} +1 -1
  24. package/dist/gateway/static/root/assets/channels-settings-B4AtvQHk.js +1 -0
  25. package/dist/gateway/static/root/assets/channels-status-swr-DMxB7slL.js +1 -0
  26. package/dist/gateway/static/root/assets/{chat-terminal-dock-2fpzufzt.js → chat-terminal-dock-BRrL2iHA.js} +2 -2
  27. package/dist/gateway/static/root/assets/connectors-page-fmda2NEP.js +2 -0
  28. package/dist/gateway/static/root/assets/{cron-expression-VePLfPUR.js → cron-expression-Bl-2DzBQ.js} +1 -1
  29. package/dist/gateway/static/root/assets/{date-picker-CJegmPTJ.js → date-picker-CxTl6Wlz.js} +1 -1
  30. package/dist/gateway/static/root/assets/{dependency-picker-B2o9unBk.js → dependency-picker-DSl0mOPa.js} +1 -1
  31. package/dist/gateway/static/root/assets/{desktop-pet-BH4Np-ay.js → desktop-pet-DTeCmCdb.js} +1 -1
  32. package/dist/gateway/static/root/assets/{desktop-pet-settings-Duu3ctxD.js → desktop-pet-settings-BNebxg0V.js} +1 -1
  33. package/dist/gateway/static/root/assets/{directory-picker-path-field-BBCUZOg2.js → directory-picker-path-field-Dn9XpmXY.js} +1 -1
  34. package/dist/gateway/static/root/assets/dist-DaxPZj9e.js +1 -0
  35. package/dist/gateway/static/root/assets/{dist-DY4Af0fY.js → dist-J_ySZvz0.js} +1 -1
  36. package/dist/gateway/static/root/assets/{extension-debug-page-DS_o3F97.js → extension-debug-page-D-J-eOTb.js} +1 -1
  37. package/dist/gateway/static/root/assets/{extension-page-BTLBZdCq.js → extension-page-CpST24nU.js} +1 -1
  38. package/dist/gateway/static/root/assets/{extension-settings-page-KTGrYbgk.js → extension-settings-page-4nQXFnFq.js} +1 -1
  39. package/dist/gateway/static/root/assets/floating-ui.dom-B7uQOZZW.js +1 -0
  40. package/dist/gateway/static/root/assets/{gateway-config-api-D3YJVfzY.js → gateway-config-api-BHWHduDi.js} +1 -1
  41. package/dist/gateway/static/root/assets/{gateway-config-swr-BzTbnu4O.js → gateway-config-swr-yUxMMVjK.js} +1 -1
  42. package/dist/gateway/static/root/assets/{gateway-settings-B8--s3yC.js → gateway-settings-BimMKmkM.js} +1 -1
  43. package/dist/gateway/static/root/assets/{gateway-startup-retry-B2zpkNvi.js → gateway-startup-retry-9mrsL84j.js} +1 -1
  44. package/dist/gateway/static/root/assets/heartbeat-settings-MUJ3mEC9.js +1 -0
  45. package/dist/gateway/static/root/assets/home-page-DSM6aPHF.js +1 -0
  46. package/dist/gateway/static/root/assets/{image-generation-api-BXRjz-iY.js → image-generation-api-CgFAGDi8.js} +1 -1
  47. package/dist/gateway/static/root/assets/index-B3zYJBhK.js +74 -0
  48. package/dist/gateway/static/root/assets/index-D3_xSYKd.css +2 -0
  49. package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-D-Ib-suu.js → keyboard-shortcuts-settings-6Ar5rfX4.js} +1 -1
  50. package/dist/gateway/static/root/assets/local-app-workbench-page-tzM0wTL8.js +2 -0
  51. package/dist/gateway/static/root/assets/{local-apps-page-Bsy1lvAc.js → local-apps-page-B5n-x544.js} +1 -1
  52. package/dist/gateway/static/root/assets/{locale-store-iKjqemWG.js → locale-store-CPylQjH9.js} +1 -1
  53. package/dist/gateway/static/root/assets/logs-page-CgoQlera.js +2 -0
  54. package/dist/gateway/static/root/assets/{management-settings-D-akydG4.js → management-settings-CXBLyU-0.js} +1 -1
  55. package/dist/gateway/static/root/assets/{markdown-split-DHH4Cu2i.js → markdown-split-CtmQemJi.js} +1 -1
  56. package/dist/gateway/static/root/assets/markdown-view-CUsob4Pz.js +69 -0
  57. package/dist/gateway/static/root/assets/{media-plugins-BwHtUXkj.js → media-plugins-Dsd-IEOW.js} +1 -1
  58. package/dist/gateway/static/root/assets/messages-B82nsi9k.js +3 -0
  59. package/dist/gateway/static/root/assets/notes-api-CfKZYrBC.js +1 -0
  60. package/dist/gateway/static/root/assets/{notes-page-BxM1p6hz.js → notes-page-BKbOTE2n.js} +1 -1
  61. package/dist/gateway/static/root/assets/notes-workbench-BScELo01.js +4 -0
  62. package/dist/gateway/static/root/assets/oauth-authorization-window-Cdolr2wE.js +1 -0
  63. package/dist/gateway/static/root/assets/{popover-select-BzdnTy3h.js → popover-select-CccGri5Y.js} +1 -1
  64. package/dist/gateway/static/root/assets/{product-open-page-DrfWH1ir.js → product-open-page-DqB9bsDB.js} +1 -1
  65. package/dist/gateway/static/root/assets/project-detail-page-DUoyty8X.js +1 -0
  66. package/dist/gateway/static/root/assets/projects-page-PeXUdGdO.js +1 -0
  67. package/dist/gateway/static/root/assets/{registry-api-Cx3JxXSL.js → registry-api-B_5D8epF.js} +1 -1
  68. package/dist/gateway/static/root/assets/remote-access-hub-sHho-uXP.js +1 -0
  69. package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-DF7syQRd.js → runtime-tools-settings-panel-DBTH_t0O.js} +1 -1
  70. package/dist/gateway/static/root/assets/{schema-form-BQ_qRIsM.js → schema-form-CqMKAwV0.js} +1 -1
  71. package/dist/gateway/static/root/assets/sessions-page-DyvNgaP9.js +1 -0
  72. package/dist/gateway/static/root/assets/settings-advanced-gate-CnYf1R7z.js +1 -0
  73. package/dist/gateway/static/root/assets/{settings-form-section-BBHGOQHU.js → settings-form-section-dPriuuYq.js} +1 -1
  74. package/dist/gateway/static/root/assets/{settings-loading-skeleton-CiwjP2-b.js → settings-loading-skeleton-B-Mbm8Va.js} +1 -1
  75. package/dist/gateway/static/root/assets/{settings-page-Rl9waapk.js → settings-page-Dyj3w-Ml.js} +1 -1
  76. package/dist/gateway/static/root/assets/setup-status-panel-C4FxY5J3.js +1 -0
  77. package/dist/gateway/static/root/assets/share-preview-page-emEdx7YL.js +2 -0
  78. package/dist/gateway/static/root/assets/shares-settings-BQBVq-sN.js +1 -0
  79. package/dist/gateway/static/root/assets/skill-reload-api-BFoEpQ4I.js +1 -0
  80. package/dist/gateway/static/root/assets/{skills-page-YbtR_zEU.js → skills-page-BNtYnrRT.js} +1 -1
  81. package/dist/gateway/static/root/assets/{skills-page.utils-DTmx2Zza.js → skills-page.utils-DjnbNFs5.js} +1 -1
  82. package/dist/gateway/static/root/assets/{system-settings-panel-BZDJcKdS.js → system-settings-panel-DDStf-jZ.js} +1 -1
  83. package/dist/gateway/static/root/assets/task-detail-page-ZVwr8KbS.js +1 -0
  84. package/dist/gateway/static/root/assets/{theme-store-BOD2D0dJ.js → theme-store-BN-32Fz5.js} +1 -1
  85. package/dist/gateway/static/root/assets/{use-autosave-Be9aR9qA.js → use-autosave-C2mKZ-ua.js} +1 -1
  86. package/dist/gateway/static/root/assets/user-context-page-CHjIinCW.js +1 -0
  87. package/dist/gateway/static/root/assets/{voice-api-key-field-IMc3Ua6C.js → voice-api-key-field-D7OHeQby.js} +1 -1
  88. package/dist/gateway/static/root/assets/{work-discovery-overlay-TC-xQenr.js → work-discovery-overlay-CIiXie7F.js} +1 -1
  89. package/dist/gateway/static/root/assets/{workflow-run-setup-panel-B2auxcRv.js → workflow-run-setup-panel-9_dAGW3_.js} +2 -2
  90. package/dist/gateway/static/root/assets/workflows-page-DXpYJxao.js +5 -0
  91. package/dist/gateway/static/root/index.html +15 -14
  92. package/dist/package.js +1 -1
  93. package/dist/src/agent/agent-instance-gateway.d.ts +3 -0
  94. package/dist/src/agent/agent-manager.d.ts +3 -0
  95. package/dist/src/agent/agent-manager.js +30 -24
  96. package/dist/src/agent/embedded/index.d.ts +1 -1
  97. package/dist/src/agent/embedded/index.js +2 -2
  98. package/dist/src/agent/embedded/run-for-session.js +101 -62
  99. package/dist/src/agent/embedded/run-turn.js +56 -28
  100. package/dist/src/agent/embedded/runs.d.ts +16 -29
  101. package/dist/src/agent/embedded/runs.js +44 -65
  102. package/dist/src/agent/embedded/types.d.ts +4 -0
  103. package/dist/src/agent/orchestration/agent-run-supervisor.d.ts +28 -0
  104. package/dist/src/agent/orchestration/agent-run-supervisor.js +49 -0
  105. package/dist/src/agent/orchestration/agent-turn-policy.d.ts +18 -0
  106. package/dist/src/agent/orchestration/agent-turn-policy.js +36 -0
  107. package/dist/src/agent/orchestration/llm-turn-retry.d.ts +5 -0
  108. package/dist/src/agent/orchestration/llm-turn-retry.js +34 -4
  109. package/dist/src/agent/orchestration/run-agent-turn-with-timeout.d.ts +10 -1
  110. package/dist/src/agent/orchestration/run-agent-turn-with-timeout.js +25 -4
  111. package/dist/src/agent/service/async-queue.d.ts +16 -3
  112. package/dist/src/agent/service/async-queue.js +29 -11
  113. package/dist/src/agent/service/process-direct-streaming.js +27 -4
  114. package/dist/src/agent/service.js +1 -0
  115. package/dist/src/agent/tools/exec-command.d.ts +2 -0
  116. package/dist/src/agent/tools/exec-command.js +70 -7
  117. package/dist/src/auth/oauth/index.js +3 -3
  118. package/dist/src/auth/oauth/registry.d.ts +2 -0
  119. package/dist/src/auth/oauth/registry.js +20 -12
  120. package/dist/src/auth/oauth/xopc-cloud.d.ts +1 -0
  121. package/dist/src/auth/oauth/xopc-cloud.js +32 -16
  122. package/dist/src/cli/commands/channels.js +1 -1
  123. package/dist/src/cli/commands/connectors.js +1 -1
  124. package/dist/src/cli/commands/extension-marketplace.js +1 -1
  125. package/dist/src/cli/commands/models.js +1 -1
  126. package/dist/src/cli/commands/onboard.js +1 -1
  127. package/dist/src/cli/commands/tunnel.js +4 -5
  128. package/dist/src/config/agent-profile.js +1 -1
  129. package/dist/src/config/schema.d.ts +18 -0
  130. package/dist/src/config/schema.js +19 -1
  131. package/dist/src/extensions/sdk/index.js +1 -1
  132. package/dist/src/gateway/agents-admin.js +1 -1
  133. package/dist/src/gateway/hono/app.js +2 -1
  134. package/dist/src/gateway/hono/oauth-async.d.ts +1 -0
  135. package/dist/src/gateway/hono/oauth-async.js +11 -4
  136. package/dist/src/gateway/hono/routes/config.js +10 -0
  137. package/dist/src/gateway/hono/routes/host-fs.js +40 -1
  138. package/dist/src/gateway/hono/routes/media.js +33 -1
  139. package/dist/src/gateway/hono/routes/notes.js +162 -7
  140. package/dist/src/gateway/hono/routes/shares.js +141 -6
  141. package/dist/src/gateway/hono/routes/tunnel.js +47 -10
  142. package/dist/src/gateway/hono/routes/voice.d.ts +2 -6
  143. package/dist/src/gateway/hono/routes/voice.js +20 -4
  144. package/dist/src/gateway/hono/routes/workflows.js +2 -2
  145. package/dist/src/gateway/run-loop.js +3 -0
  146. package/dist/src/gateway/server.d.ts +3 -0
  147. package/dist/src/gateway/server.js +30 -5
  148. package/dist/src/gateway/service.js +4 -4
  149. package/dist/src/notes/service.d.ts +6 -2
  150. package/dist/src/notes/service.js +36 -4
  151. package/dist/src/providers/index.js +2 -2
  152. package/dist/src/share/note-share-service.d.ts +67 -0
  153. package/dist/src/share/note-share-service.js +283 -0
  154. package/dist/src/share/share-config.js +32 -3
  155. package/dist/src/share/share-landing.d.ts +5 -1
  156. package/dist/src/share/share-landing.js +28 -1
  157. package/dist/src/share/share-store.d.ts +34 -5
  158. package/dist/src/share/share-store.js +125 -19
  159. package/dist/src/share/share-thumbnail.js +4 -2
  160. package/dist/src/share/share-types.d.ts +52 -14
  161. package/dist/src/share/share-types.js +9 -0
  162. package/dist/src/share/share-zip.d.ts +2 -2
  163. package/dist/src/tui/backends/embedded-backend.js +1 -1
  164. package/dist/src/tunnel/env.d.ts +12 -6
  165. package/dist/src/tunnel/env.js +34 -26
  166. package/dist/src/tunnel/gateway-lifecycle.js +2 -13
  167. package/dist/src/tunnel/index.d.ts +1 -1
  168. package/dist/src/tunnel/index.js +2 -2
  169. package/dist/src/tunnel/tunnel-audit.d.ts +1 -1
  170. package/dist/src/tunnel/tunnel-config.d.ts +1 -1
  171. package/dist/src/tunnel/tunnel-config.js +9 -4
  172. package/dist/src/tunnel/tunnel-service.d.ts +1 -1
  173. package/dist/src/tunnel/tunnel-service.js +3 -0
  174. package/dist/src/tunnel/well-known.d.ts +1 -1
  175. package/dist/src/tunnel/well-known.js +1 -1
  176. package/dist/src/tunnel/xopc-cloud-registration.d.ts +9 -0
  177. package/dist/src/tunnel/xopc-cloud-registration.js +42 -0
  178. package/dist/src/voice/stt/transcribe-core.js +17 -1
  179. package/dist/src/voice/stt/types.d.ts +1 -1
  180. package/package.json +2 -2
  181. package/dist/gateway/static/root/assets/agents-DijFbC4G.js +0 -68
  182. package/dist/gateway/static/root/assets/apps-page-BFAmf9hq.js +0 -1
  183. package/dist/gateway/static/root/assets/automations-workspace-B8us5JBf.js +0 -6
  184. package/dist/gateway/static/root/assets/block-editor-C3mwGsms.js +0 -184
  185. package/dist/gateway/static/root/assets/browser-settings-page-BeBd17-3.js +0 -8
  186. package/dist/gateway/static/root/assets/capabilities-settings-panel-Bv0z3KRq.js +0 -7
  187. package/dist/gateway/static/root/assets/capability-presets-settings-panel-BczD-pWE.js +0 -3
  188. package/dist/gateway/static/root/assets/channels-settings-2KQYq6xl.js +0 -1
  189. package/dist/gateway/static/root/assets/channels-status-swr-ByRbxC6z.js +0 -1
  190. package/dist/gateway/static/root/assets/connectors-page-PB1boaNy.js +0 -2
  191. package/dist/gateway/static/root/assets/dist-C230izr8.js +0 -1
  192. package/dist/gateway/static/root/assets/heartbeat-settings-5YQ5BUwp.js +0 -1
  193. package/dist/gateway/static/root/assets/home-page-DfIVWJl3.js +0 -1
  194. package/dist/gateway/static/root/assets/index-BXGC3VFm.css +0 -2
  195. package/dist/gateway/static/root/assets/index-jqDCfet4.js +0 -74
  196. package/dist/gateway/static/root/assets/local-app-workbench-page-CAb0odzu.js +0 -2
  197. package/dist/gateway/static/root/assets/logs-page-CFxKbDZl.js +0 -2
  198. package/dist/gateway/static/root/assets/markdown-view-DYkxMtJA.js +0 -69
  199. package/dist/gateway/static/root/assets/messages-kx5W39dO.js +0 -3
  200. package/dist/gateway/static/root/assets/notes-api-DJizatxh.js +0 -1
  201. package/dist/gateway/static/root/assets/notes-workbench-BGjEC89c.js +0 -4
  202. package/dist/gateway/static/root/assets/project-detail-page-Dj9kEQwg.js +0 -1
  203. package/dist/gateway/static/root/assets/projects-page-CBRy5Ezz.js +0 -1
  204. package/dist/gateway/static/root/assets/remote-access-hub-DSwz9RBh.js +0 -1
  205. package/dist/gateway/static/root/assets/sessions-page-WtattyVX.js +0 -1
  206. package/dist/gateway/static/root/assets/settings-advanced-gate-CNAz_q1F.js +0 -1
  207. package/dist/gateway/static/root/assets/setup-status-panel-CNo4Ajhn.js +0 -1
  208. package/dist/gateway/static/root/assets/share-preview-page-B7PI3yZ6.js +0 -2
  209. package/dist/gateway/static/root/assets/shares-settings-DpOH0zWB.js +0 -1
  210. package/dist/gateway/static/root/assets/skill-reload-api-BOf4xCat.js +0 -1
  211. package/dist/gateway/static/root/assets/task-detail-page-DrMX5Yhj.js +0 -1
  212. package/dist/gateway/static/root/assets/user-context-page-ewIK2ddC.js +0 -1
  213. package/dist/gateway/static/root/assets/workflows-page-Bdn82kFQ.js +0 -5
@@ -1,7 +1,14 @@
1
1
  import { ObjectLinkService } from "../../../activity/service.js";
2
2
  import { buildSessionKey, init_session_key } from "../../../routing/session-key.js";
3
3
  import { agentExists, getDefaultAgentId, init_resolve_route } from "../../../routing/resolve-route.js";
4
+ import { resolveGatewayEffectiveHost } from "../../../config/gateway-bind.js";
5
+ import { getShareStore } from "../../../share/share-store.js";
6
+ import { resolveShareUrl } from "../../../share/share-url.js";
7
+ import { resolveReverseProxyPublicUrl } from "../../public-url.js";
8
+ import { extractToken } from "../../auth.js";
9
+ import { NoteShareService, NoteShareVersionConflictError } from "../../../share/note-share-service.js";
4
10
  import { createReadStream } from "node:fs";
11
+ import { createHash } from "node:crypto";
5
12
  import { access, stat } from "node:fs/promises";
6
13
  import { Readable } from "node:stream";
7
14
  import { stream } from "hono/streaming";
@@ -38,6 +45,9 @@ function noteNotFound() {
38
45
  code: "note_not_found"
39
46
  };
40
47
  }
48
+ function readIdempotencyKey(value) {
49
+ return value?.trim() || void 0;
50
+ }
41
51
  function parseCaptureSource(body) {
42
52
  return {
43
53
  channel: typeof body.channel === "string" && VALID_CHANNELS.has(body.channel) ? body.channel : "web",
@@ -63,6 +73,21 @@ function noteThreadName(note) {
63
73
  function registerNotesRoutes(authenticated, deps) {
64
74
  const { service } = deps;
65
75
  const objectLinks = new ObjectLinkService();
76
+ const shareConfig = () => {
77
+ const raw = (service.currentConfig?.gateway)?.share;
78
+ return raw && typeof raw === "object" ? raw : {};
79
+ };
80
+ const shareStore = () => {
81
+ const store = getShareStore(shareConfig());
82
+ store.updateConfig(shareConfig());
83
+ return store;
84
+ };
85
+ const noteShares = () => new NoteShareService(shareStore(), service.notesServiceInstance);
86
+ const shareUrlContext = () => ({
87
+ gatewayHost: resolveGatewayEffectiveHost(service.currentConfig),
88
+ gatewayPort: service.currentConfig?.gateway?.port ?? 18790,
89
+ reverseProxyPublicUrl: resolveReverseProxyPublicUrl(service.currentConfig)
90
+ });
66
91
  const linkNoteToProject = (note, projectId) => {
67
92
  if (!projectId) return;
68
93
  const project = service.projects.get(projectId);
@@ -88,7 +113,7 @@ function registerNotesRoutes(authenticated, deps) {
88
113
  const text = typeof body.text === "string" ? body.text.trim() : "";
89
114
  if (!text) return c.json({ error: "Missing required field: text" }, 400);
90
115
  const source = parseCaptureSource(body);
91
- const idempotencyKey = c.req.header("idempotency-key")?.trim();
116
+ const idempotencyKey = readIdempotencyKey(c.req.header("idempotency-key"));
92
117
  if (idempotencyKey && idempotencyKey.length > 200) return c.json({ error: "Idempotency-Key is too long" }, 400);
93
118
  const note = await service.notesServiceInstance.quickCapture(text, source, idempotencyKey || void 0);
94
119
  return c.json({ note }, 201);
@@ -119,7 +144,10 @@ function registerNotesRoutes(authenticated, deps) {
119
144
  return c.json(result);
120
145
  });
121
146
  authenticated.post("/api/notes", async (c) => {
122
- if ((c.req.header("content-type") || "").includes("multipart/form-data")) {
147
+ const contentType = c.req.header("content-type") || "";
148
+ const idempotencyKey = readIdempotencyKey(c.req.header("idempotency-key"));
149
+ if (idempotencyKey && idempotencyKey.length > 200) return c.json({ error: "Idempotency-Key is too long" }, 400);
150
+ if (contentType.includes("multipart/form-data")) {
123
151
  let body;
124
152
  try {
125
153
  body = await c.req.parseBody({ all: true });
@@ -137,7 +165,7 @@ function registerNotesRoutes(authenticated, deps) {
137
165
  kind: kindRaw && VALID_KINDS.has(kindRaw) ? kindRaw : void 0,
138
166
  tags: tagsRaw ? tagsRaw.split(",").map((t) => t.trim()).filter(Boolean) : void 0,
139
167
  capturedVia: source
140
- });
168
+ }, idempotencyKey);
141
169
  linkNoteToProject(note, projectId);
142
170
  const file = body.file;
143
171
  if (file && typeof file === "object") {
@@ -156,8 +184,9 @@ function registerNotesRoutes(authenticated, deps) {
156
184
  name: fileName,
157
185
  buffer: buf,
158
186
  mimeType,
159
- duration: Number.isFinite(duration) ? duration : void 0
160
- });
187
+ duration: Number.isFinite(duration) ? duration : void 0,
188
+ retainWithoutReference: kindRaw === "voice"
189
+ }, idempotencyKey);
161
190
  }
162
191
  }
163
192
  const full = await service.notesServiceInstance.getNote(note.id);
@@ -178,7 +207,7 @@ function registerNotesRoutes(authenticated, deps) {
178
207
  tags: tagsRaw,
179
208
  capturedVia: source,
180
209
  pinned: body.pinned === true
181
- });
210
+ }, idempotencyKey);
182
211
  linkNoteToProject(note, projectId);
183
212
  return c.json({ note }, 201);
184
213
  });
@@ -335,6 +364,127 @@ function registerNotesRoutes(authenticated, deps) {
335
364
  if (!note) return c.json(noteNotFound(), 404);
336
365
  return c.json({ note });
337
366
  });
367
+ authenticated.get("/api/notes/:id/shares", async (c) => {
368
+ const note = await service.notesServiceInstance.getNote(c.req.param("id"));
369
+ if (!note) return c.json(noteNotFound(), 404);
370
+ const shares = noteShares();
371
+ const now = Date.now();
372
+ const items = shares.list(note.id).map((record) => {
373
+ const resolved = resolveShareUrl(record.token, shareUrlContext());
374
+ return {
375
+ id: record.id,
376
+ kind: "note",
377
+ fileName: record.fileName,
378
+ shareUrl: resolved.shareUrl,
379
+ lanUrl: resolved.lanUrl,
380
+ reachability: resolved.reachability,
381
+ reachabilityHint: resolved.reachabilityHint,
382
+ createdAt: record.createdAt,
383
+ expiresAt: record.expiresAt,
384
+ viewCount: record.downloadCount,
385
+ maxViews: record.maxViews,
386
+ revoked: record.revoked,
387
+ expired: now >= new Date(record.expiresAt).getTime(),
388
+ description: record.description ?? null,
389
+ sourceVersion: record.sourceVersion,
390
+ snapshotRevision: record.snapshotRevision,
391
+ attachmentCount: record.attachmentCount,
392
+ stale: note.updatedAt > record.sourceVersion
393
+ };
394
+ });
395
+ return c.json({
396
+ items,
397
+ total: items.length,
398
+ noteVersion: note.updatedAt
399
+ });
400
+ });
401
+ authenticated.post("/api/notes/:id/shares", async (c) => {
402
+ const gatewayToken = extractToken({ authorization: c.req.header("authorization") ?? void 0 });
403
+ if (!gatewayToken) return c.json({
404
+ ok: false,
405
+ error: { message: "Token required" }
406
+ }, 401);
407
+ const body = await c.req.json().catch(() => ({}));
408
+ const shares = noteShares();
409
+ try {
410
+ const record = await shares.create(c.req.param("id"), {
411
+ expectedNoteVersion: typeof body.expectedNoteVersion === "number" ? body.expectedNoteVersion : void 0,
412
+ attachmentIds: Array.isArray(body.attachmentIds) ? body.attachmentIds.filter((id) => typeof id === "string") : void 0,
413
+ ttlMs: typeof body.ttlMs === "number" ? body.ttlMs : void 0,
414
+ maxViews: body.maxViews === null ? null : typeof body.maxViews === "number" ? body.maxViews : void 0,
415
+ description: typeof body.description === "string" ? body.description.trim() || void 0 : void 0,
416
+ gatewayTokenHash: createHash("sha256").update(gatewayToken, "utf8").digest("hex").slice(0, 12)
417
+ });
418
+ const resolved = resolveShareUrl(record.token, shareUrlContext());
419
+ return c.json({
420
+ ok: true,
421
+ payload: {
422
+ id: record.id,
423
+ kind: record.kind,
424
+ shareUrl: resolved.shareUrl,
425
+ lanUrl: resolved.lanUrl,
426
+ reachability: resolved.reachability,
427
+ reachabilityHint: resolved.reachabilityHint,
428
+ expiresAt: record.expiresAt,
429
+ maxViews: record.maxViews,
430
+ sourceNoteId: record.sourceNoteId,
431
+ sourceVersion: record.sourceVersion,
432
+ snapshotRevision: record.snapshotRevision,
433
+ attachmentCount: record.attachmentCount,
434
+ fileName: record.fileName
435
+ }
436
+ }, 201);
437
+ } catch (err) {
438
+ if (err instanceof NoteShareVersionConflictError) return c.json({
439
+ ok: false,
440
+ error: {
441
+ code: "note_version_conflict",
442
+ message: err.message,
443
+ currentVersion: err.currentVersion
444
+ }
445
+ }, 409);
446
+ const message = err instanceof Error ? err.message : String(err);
447
+ return c.json({
448
+ ok: false,
449
+ error: { message }
450
+ }, message === "Note not found" ? 404 : 400);
451
+ }
452
+ });
453
+ authenticated.post("/api/notes/:id/shares/:shareId/refresh", async (c) => {
454
+ const body = await c.req.json().catch(() => ({}));
455
+ const shares = noteShares();
456
+ try {
457
+ const record = await shares.refresh(c.req.param("id"), c.req.param("shareId"), {
458
+ expectedNoteVersion: typeof body.expectedNoteVersion === "number" ? body.expectedNoteVersion : void 0,
459
+ attachmentIds: Array.isArray(body.attachmentIds) ? body.attachmentIds.filter((id) => typeof id === "string") : void 0
460
+ });
461
+ const resolved = resolveShareUrl(record.token, shareUrlContext());
462
+ return c.json({
463
+ ok: true,
464
+ payload: {
465
+ id: record.id,
466
+ shareUrl: resolved.shareUrl,
467
+ sourceVersion: record.sourceVersion,
468
+ snapshotRevision: record.snapshotRevision,
469
+ attachmentCount: record.attachmentCount
470
+ }
471
+ });
472
+ } catch (err) {
473
+ if (err instanceof NoteShareVersionConflictError) return c.json({
474
+ ok: false,
475
+ error: {
476
+ code: "note_version_conflict",
477
+ message: err.message,
478
+ currentVersion: err.currentVersion
479
+ }
480
+ }, 409);
481
+ const message = err instanceof Error ? err.message : String(err);
482
+ return c.json({
483
+ ok: false,
484
+ error: { message }
485
+ }, message === "Note share not found" || message === "Note not found" ? 404 : 400);
486
+ }
487
+ });
338
488
  authenticated.patch("/api/notes/:id", async (c) => {
339
489
  const id = c.req.param("id");
340
490
  const body = await c.req.json().catch(() => ({}));
@@ -346,8 +496,13 @@ function registerNotesRoutes(authenticated, deps) {
346
496
  });
347
497
  authenticated.delete("/api/notes/:id", async (c) => {
348
498
  const id = c.req.param("id");
499
+ const body = await c.req.json().catch(() => ({}));
349
500
  if (!await service.notesServiceInstance.deleteNote(id)) return c.json(noteNotFound(), 404);
350
- return c.json({ deleted: true });
501
+ const revokedShares = body.revokeShares === false ? 0 : await noteShares().revokeForNote(id);
502
+ return c.json({
503
+ deleted: true,
504
+ revokedShares
505
+ });
351
506
  });
352
507
  authenticated.get("/api/notes/:id/history", async (c) => {
353
508
  const id = c.req.param("id");
@@ -10,7 +10,8 @@ import { audienceDefaults, cleanupStagedSite, decideShareKind, forgetStagedSite,
10
10
  import { deleteThumbnail, placeholderSvg, readThumbnail, scheduleThumbnail, thumbnailContentType, thumbnailExists } from "../../../share/share-thumbnail.js";
11
11
  import { extractToken } from "../../auth.js";
12
12
  import { getClientIpFromHeaders } from "../../security/loopback.js";
13
- import { renderFolderLandingPage, renderShareExpiredPage, renderShareLandingPage } from "../../../share/share-landing.js";
13
+ import { renderFolderLandingPage, renderNoteShareLandingPage, renderShareExpiredPage, renderShareLandingPage } from "../../../share/share-landing.js";
14
+ import { NoteShareService } from "../../../share/note-share-service.js";
14
15
  import { consumeSharePublicLimit } from "../../../share/share-rate-limit.js";
15
16
  import { createZipStream, planDirectoryFiles } from "../../../share/share-zip.js";
16
17
  import { createReadStream } from "node:fs";
@@ -93,6 +94,7 @@ function rfc5987ContentDisposition(disposition, fileName) {
93
94
  }
94
95
  function registerSharePublicRoutes(app, service) {
95
96
  const store = getShareStore(resolveShareConfig(service));
97
+ const noteShares = new NoteShareService(store, service.notesServiceInstance);
96
98
  /** Landing page — does NOT consume downloadCount. */
97
99
  app.get("/s/:token", async (c) => {
98
100
  const clientIp = getClientIpFromHeaders({ get: (n) => c.req.header(n) ?? void 0 });
@@ -115,6 +117,13 @@ function registerSharePublicRoutes(app, service) {
115
117
  return c.html(renderShareExpiredPage(validation.reason), 410);
116
118
  }
117
119
  if (record.kind === "directory") return renderDirectoryLanding(c, store, service, record, token);
120
+ if (record.kind === "note") {
121
+ const previewUrl = `/#/share/${encodeURIComponent(token)}`;
122
+ c.header("Content-Security-Policy", "default-src 'none'; script-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'");
123
+ c.header("X-Frame-Options", "DENY");
124
+ c.header("Referrer-Policy", "no-referrer");
125
+ return c.html(renderNoteShareLandingPage(record, previewUrl, { og: buildLandingOg(service, record) }));
126
+ }
118
127
  if (c.req.query("dl") === "1") return handleFileDownload(c, store, record, clientIp);
119
128
  if (c.req.query("inline") === "1") {
120
129
  if ({
@@ -157,9 +166,105 @@ function registerSharePublicRoutes(app, service) {
157
166
  }, `Share download denied: ${validation.reason}`);
158
167
  return c.html(renderShareExpiredPage(validation.reason), 410);
159
168
  }
160
- if (record.kind === "directory") return c.html(renderShareExpiredPage("not_found"), 404);
169
+ if (record.kind !== "file") return c.html(renderShareExpiredPage("not_found"), 404);
161
170
  return handleFileDownload(c, store, record, clientIp);
162
171
  });
172
+ /** Claim one Note view. POST prevents social unfurlers from consuming maxViews. */
173
+ app.post("/s/:token/view", async (c) => {
174
+ const clientIp = getClientIpFromHeaders({ get: (n) => c.req.header(n) ?? void 0 });
175
+ const rateResult = consumeSharePublicLimit(clientIp);
176
+ if (!rateResult.allowed) {
177
+ c.header("Retry-After", String(Math.ceil(rateResult.retryAfterMs / 1e3)));
178
+ return c.json({
179
+ ok: false,
180
+ error: { message: "rate_limited" }
181
+ }, 429);
182
+ }
183
+ const token = c.req.param("token");
184
+ const record = store.getByToken(token);
185
+ if (!record || record.kind !== "note") return c.json({
186
+ ok: false,
187
+ error: { message: "not_found" }
188
+ }, 404);
189
+ const validation = store.validateAccess(record);
190
+ if (!validation.valid) return c.json({
191
+ ok: false,
192
+ error: { message: validation.reason }
193
+ }, 410);
194
+ try {
195
+ const manifest = await noteShares.readManifest(record);
196
+ const consumed = store.consumeAccess(record.id);
197
+ if (consumed.valid === false) return c.json({
198
+ ok: false,
199
+ error: { message: consumed.reason }
200
+ }, 410);
201
+ const ticket = noteShares.issueAssetTicket(record);
202
+ logShareAudit("share.access", {
203
+ shareId: record.id,
204
+ noteId: record.sourceNoteId,
205
+ tokenPrefix: token.slice(0, 8),
206
+ clientIp,
207
+ snapshotRevision: record.snapshotRevision
208
+ }, `Note share viewed: ${record.fileName}`);
209
+ return c.json({
210
+ ok: true,
211
+ payload: {
212
+ kind: "note",
213
+ title: manifest.title,
214
+ markdown: noteShares.publicMarkdown(record, manifest, ticket),
215
+ snapshotAt: manifest.snapshotAt,
216
+ expiresAt: record.expiresAt,
217
+ description: record.description ?? null,
218
+ sourceVersion: record.sourceVersion,
219
+ snapshotRevision: record.snapshotRevision,
220
+ attachments: manifest.attachments.map(({ artifactFileName: _artifactFileName, checksum: _checksum, ...attachment }) => attachment)
221
+ }
222
+ });
223
+ } catch (err) {
224
+ logShareAudit("share.access_denied", {
225
+ shareId: record.id,
226
+ noteId: record.sourceNoteId,
227
+ tokenPrefix: token.slice(0, 8),
228
+ clientIp,
229
+ reason: "artifact_missing"
230
+ }, "Note share artifact could not be read");
231
+ return c.json({
232
+ ok: false,
233
+ error: { message: err instanceof Error ? err.message : "artifact_missing" }
234
+ }, 410);
235
+ }
236
+ });
237
+ /** Serve one snapshotted Note attachment through a short-lived view ticket. */
238
+ app.get("/s/:token/assets/:attachmentId", async (c) => {
239
+ const rateResult = consumeSharePublicLimit(getClientIpFromHeaders({ get: (n) => c.req.header(n) ?? void 0 }));
240
+ if (!rateResult.allowed) {
241
+ c.header("Retry-After", String(Math.ceil(rateResult.retryAfterMs / 1e3)));
242
+ return c.text("Too many requests", 429);
243
+ }
244
+ const record = store.getByToken(c.req.param("token"));
245
+ if (!record || record.kind !== "note") return c.text("Not found", 404);
246
+ if (record.revoked || Date.now() >= new Date(record.expiresAt).getTime()) return c.text("Gone", 410);
247
+ const ticket = c.req.query("ticket") ?? "";
248
+ if (!noteShares.verifyAssetTicket(record, ticket)) return c.text("Forbidden", 403);
249
+ const resolved = await noteShares.resolveAsset(record, c.req.param("attachmentId")).catch(() => null);
250
+ if (!resolved) return c.text("Not found", 404);
251
+ const fileStat = await stat(resolved.path).catch(() => null);
252
+ if (!fileStat?.isFile()) return c.text("Not found", 404);
253
+ const inline = resolved.attachment.type !== "file";
254
+ const stream = createReadStream(resolved.path);
255
+ return new Response(Readable.toWeb(stream), {
256
+ status: 200,
257
+ headers: {
258
+ "Content-Type": shareResponseContentType(resolved.attachment.mimeType),
259
+ "Content-Disposition": rfc5987ContentDisposition(inline ? "inline" : "attachment", resolved.attachment.fileName),
260
+ "Content-Length": String(fileStat.size),
261
+ "Cache-Control": "private, no-store",
262
+ "X-Content-Type-Options": "nosniff",
263
+ "X-Frame-Options": "DENY",
264
+ "Referrer-Policy": "no-referrer"
265
+ }
266
+ });
267
+ });
163
268
  /** Directory child file (GET — preview counts as download per product). */
164
269
  app.get("/s/:token/file", async (c) => {
165
270
  const clientIp = getClientIpFromHeaders({ get: (n) => c.req.header(n) ?? void 0 });
@@ -251,7 +356,12 @@ function registerSharePublicRoutes(app, service) {
251
356
  expiresAt: record.expiresAt,
252
357
  remainingViews,
253
358
  valid: validation.valid,
254
- directory: record.directory ?? null
359
+ directory: record.directory ?? null,
360
+ ...record.kind === "note" ? {
361
+ sourceVersion: record.sourceVersion,
362
+ snapshotRevision: record.snapshotRevision,
363
+ attachmentCount: record.attachmentCount
364
+ } : {}
255
365
  });
256
366
  });
257
367
  /** HEAD check. */
@@ -282,6 +392,17 @@ function registerSharePublicRoutes(app, service) {
282
392
  "X-Content-Type-Options": "nosniff"
283
393
  }
284
394
  });
395
+ if (record.kind === "note") {
396
+ const placeholder = placeholderSvg(record.fileName, "text");
397
+ return new Response(placeholder, {
398
+ status: 200,
399
+ headers: {
400
+ "Content-Type": "image/svg+xml; charset=utf-8",
401
+ "Cache-Control": "public, max-age=60",
402
+ "X-Content-Type-Options": "nosniff"
403
+ }
404
+ });
405
+ }
285
406
  scheduleThumbnail({
286
407
  scope: "file",
287
408
  token,
@@ -352,6 +473,7 @@ function registerSharePublicRoutes(app, service) {
352
473
  function registerShareRoutes(authenticated, deps) {
353
474
  const { service } = deps;
354
475
  const store = getShareStore(resolveShareConfig(service));
476
+ const noteShares = new NoteShareService(store, service.notesServiceInstance);
355
477
  getSiteShareStore(resolveSiteShareConfig(service)).setCleanupHook((rec) => {
356
478
  const dir = forgetStagedSite(rec.id);
357
479
  if (dir) cleanupStagedSite(dir);
@@ -628,7 +750,13 @@ function registerShareRoutes(authenticated, deps) {
628
750
  description: r.description ?? null,
629
751
  fileSize: r.fileSize,
630
752
  mimeType: r.mimeType,
631
- directory: r.directory ?? null
753
+ directory: r.directory ?? null,
754
+ ...r.kind === "note" ? {
755
+ sourceNoteId: r.sourceNoteId,
756
+ sourceVersion: r.sourceVersion,
757
+ snapshotRevision: r.snapshotRevision,
758
+ attachmentCount: r.attachmentCount
759
+ } : {}
632
760
  };
633
761
  });
634
762
  return c.json({
@@ -646,11 +774,12 @@ function registerShareRoutes(authenticated, deps) {
646
774
  const urlCtx = getShareUrlContext(service);
647
775
  const resolved = resolveShareUrl(record.token, urlCtx);
648
776
  const expired = Date.now() >= new Date(record.expiresAt).getTime();
777
+ const { token: _token, createdByTokenHash: _createdByTokenHash, ...safeRecord } = record;
778
+ if ("assetTicketSecret" in safeRecord) delete safeRecord.assetTicketSecret;
649
779
  return c.json({
650
780
  ok: true,
651
781
  payload: {
652
- ...record,
653
- token: void 0,
782
+ ...safeRecord,
654
783
  shareUrl: resolved.shareUrl,
655
784
  lanUrl: resolved.lanUrl,
656
785
  reachability: resolved.reachability,
@@ -660,10 +789,12 @@ function registerShareRoutes(authenticated, deps) {
660
789
  });
661
790
  authenticated.delete("/api/shares/:id", async (c) => {
662
791
  const id = c.req.param("id");
792
+ const record = store.getById(id);
663
793
  if (!store.revoke(id)) return c.json({
664
794
  ok: false,
665
795
  error: { message: "Not found" }
666
796
  }, 404);
797
+ if (record?.kind === "note") await noteShares.removeArtifact(record.id);
667
798
  return c.json({ ok: true });
668
799
  });
669
800
  authenticated.delete("/api/shares", async (c) => {
@@ -672,7 +803,9 @@ function registerShareRoutes(authenticated, deps) {
672
803
  body = await c.req.json();
673
804
  } catch {}
674
805
  if (body.expired === true) {
806
+ const expiredNoteShares = store.getAllShares().filter((record) => record.kind === "note" && !record.revoked && Date.now() >= new Date(record.expiresAt).getTime());
675
807
  const count = store.revokeExpired();
808
+ await Promise.all(expiredNoteShares.map((record) => noteShares.removeArtifact(record.id)));
676
809
  return c.json({
677
810
  ok: true,
678
811
  payload: { revokedCount: count }
@@ -683,7 +816,9 @@ function registerShareRoutes(authenticated, deps) {
683
816
  ok: false,
684
817
  error: { message: "Provide ids array or expired: true" }
685
818
  }, 400);
819
+ const noteRecords = ids.map((id) => store.getById(id)).filter((record) => record?.kind === "note");
686
820
  const count = store.revokeMany(ids);
821
+ await Promise.all(noteRecords.map((record) => noteShares.removeArtifact(record.id)));
687
822
  return c.json({
688
823
  ok: true,
689
824
  payload: { revokedCount: count }
@@ -4,12 +4,12 @@ import { loadTunnelState } from "../../../tunnel/tunnel-state.js";
4
4
  import { resolveReverseProxyPublicUrl } from "../../public-url.js";
5
5
  import { extractToken } from "../../auth.js";
6
6
  import { TUNNEL_CONSENT_REQUIRED_CODE, TunnelConsentError, assertTunnelMayStart, getTunnelConsentState } from "../../../tunnel/consent.js";
7
- import { getTunnelRegistrationSecretMeta, readTunnelRegistrationSecretFromConfigOnly, resolveTunnelBrokerUrl } from "../../../tunnel/env.js";
7
+ import { TUNNEL_REGISTRATION_SECRET_REQUIRED_CODE, TunnelRegistrationSecretError, getTunnelRegistrationSecretMeta, readTunnelRegistrationSecretFromConfigOnly, resolveTunnelBrokerUrl } from "../../../tunnel/env.js";
8
8
  import { createPairingSecret, exchangePairingSecretOnce, getCachedPairingExchange } from "../../../tunnel/pairing.js";
9
9
  import { logTunnelAudit } from "../../../tunnel/tunnel-audit.js";
10
10
  import { getTunnelService, hashGatewayToken } from "../../../tunnel/tunnel-service.js";
11
11
  import { configureTunnelFromGatewayConfig } from "../../../tunnel/gateway-lifecycle.js";
12
- import { applyTunnelConsentToConfig, setTunnelEnabledInConfig } from "../../../tunnel/tunnel-config.js";
12
+ import { applyTunnelConsentToConfig, mergeTunnelConfigPatch, setTunnelEnabledInConfig } from "../../../tunnel/tunnel-config.js";
13
13
  import { getClientIpFromHeaders } from "../../security/loopback.js";
14
14
  import { applyLanPairingGatewayPatch } from "../../../tunnel/enable-lan-pairing.js";
15
15
  import { consumeTunnelMutationLimit } from "../../../tunnel/tunnel-rate-limit.js";
@@ -17,6 +17,7 @@ import "../../../tunnel/index.js";
17
17
  import { buildMobileConnectUrlOrder, resolveMobilePairLanUrl, validateMobilePairBaseUrl } from "../../../tunnel/pair-url.js";
18
18
  import { buildMobilePairContext } from "../../../tunnel/pair-context.js";
19
19
  import { consumePairingExchangeFailLimit } from "../../../tunnel/pairing-rate-limit.js";
20
+ import { provisionTunnelRegistrationKey } from "../../../tunnel/xopc-cloud-registration.js";
20
21
  //#region src/gateway/hono/routes/tunnel.ts
21
22
  init_public_url();
22
23
  async function configureTunnelFromService(deps, opts) {
@@ -28,6 +29,11 @@ function enrichTunnelStatus(config, status) {
28
29
  const registrationSecret = getTunnelRegistrationSecretMeta(config, process.env, brokerUrl);
29
30
  return {
30
31
  ...status,
32
+ config: {
33
+ ...status.config,
34
+ autoStart: config.tunnel?.autoStart ?? false,
35
+ brokerUrl
36
+ },
31
37
  consentRequired: consent.consentRequired,
32
38
  consent: {
33
39
  currentVersion: consent.currentVersion,
@@ -169,7 +175,6 @@ function registerTunnelRoutes(authenticated, deps) {
169
175
  const tunnel = getTunnelService();
170
176
  const tunnelMutationLimit = createTunnelMutationRateLimitMiddleware();
171
177
  authenticated.get("/api/tunnel/pair/context", async (c) => {
172
- await configureTunnelFromService(deps);
173
178
  const config = deps.service.currentConfig;
174
179
  const status = tunnel.getStatus();
175
180
  const context = buildMobilePairContext({
@@ -222,7 +227,6 @@ function registerTunnelRoutes(authenticated, deps) {
222
227
  });
223
228
  });
224
229
  authenticated.post("/api/tunnel/pair", tunnelMutationLimit, async (c) => {
225
- await configureTunnelFromService(deps);
226
230
  const token = requireGatewayToken(c);
227
231
  if (!token) return c.json({ error: "Gateway token required" }, 401);
228
232
  const { secret, expiresAt } = createPairingSecret();
@@ -314,7 +318,6 @@ function registerTunnelRoutes(authenticated, deps) {
314
318
  });
315
319
  });
316
320
  authenticated.get("/api/tunnel/status", async (c) => {
317
- await configureTunnelFromService(deps);
318
321
  const config = deps.service.currentConfig;
319
322
  return c.json(enrichTunnelStatus(config, tunnel.getStatus()));
320
323
  });
@@ -343,6 +346,29 @@ function registerTunnelRoutes(authenticated, deps) {
343
346
  }
344
347
  });
345
348
  });
349
+ authenticated.post("/api/tunnel/registration-key", tunnelMutationLimit, async (c) => {
350
+ const config = deps.service.currentConfig;
351
+ try {
352
+ const merged = mergeTunnelConfigPatch(config, { registrationSecret: await provisionTunnelRegistrationKey() });
353
+ if (merged.ok === false) return c.json({
354
+ ok: false,
355
+ error: merged.message
356
+ }, 400);
357
+ const saved = await deps.service.saveConfig(config);
358
+ if (!saved.saved) return c.json({
359
+ ok: false,
360
+ error: saved.error ?? "Failed to save tunnel registration key"
361
+ }, 500);
362
+ logTunnelAudit("tunnel.registration_authorized", { phase: "oauth_exchange" }, "Tunnel registration key created through XOPC OAuth");
363
+ return c.json({ ok: true });
364
+ } catch (error) {
365
+ const message = error instanceof Error ? error.message : String(error);
366
+ return c.json({
367
+ ok: false,
368
+ error: message
369
+ }, 502);
370
+ }
371
+ });
346
372
  authenticated.post("/api/tunnel/start", tunnelMutationLimit, async (c) => {
347
373
  await configureTunnelFromService(deps, { force: true });
348
374
  const config = deps.service.currentConfig;
@@ -367,7 +393,12 @@ function registerTunnelRoutes(authenticated, deps) {
367
393
  try {
368
394
  const qr = await tunnel.start(port, token);
369
395
  setTunnelEnabledInConfig(config, true);
370
- await deps.service.saveConfig(config);
396
+ const saved = await deps.service.saveConfig(config);
397
+ if (!saved.saved) {
398
+ await tunnel.stop();
399
+ setTunnelEnabledInConfig(config, false);
400
+ return c.json({ error: saved.error ?? "Failed to save tunnel state" }, 500);
401
+ }
371
402
  const status = tunnel.getStatus();
372
403
  return c.json({
373
404
  publicUrl: qr.publicUrl,
@@ -377,11 +408,18 @@ function registerTunnelRoutes(authenticated, deps) {
377
408
  });
378
409
  } catch (err) {
379
410
  const message = err instanceof Error ? err.message : String(err);
411
+ if (err instanceof TunnelRegistrationSecretError) return c.json({
412
+ error: message,
413
+ code: TUNNEL_REGISTRATION_SECRET_REQUIRED_CODE
414
+ }, 400);
380
415
  return c.json({ error: message }, 500);
381
416
  }
382
417
  });
383
418
  authenticated.post("/api/tunnel/stop", tunnelMutationLimit, async (c) => {
384
- await configureTunnelFromService(deps);
419
+ await configureTunnelFromService(deps, {
420
+ force: true,
421
+ deferWellKnownFetch: true
422
+ });
385
423
  const config = deps.service.currentConfig;
386
424
  let release = false;
387
425
  try {
@@ -391,14 +429,14 @@ function registerTunnelRoutes(authenticated, deps) {
391
429
  }
392
430
  const { released } = await tunnel.stop({ release });
393
431
  setTunnelEnabledInConfig(config, false);
394
- await deps.service.saveConfig(config);
432
+ const saved = await deps.service.saveConfig(config);
433
+ if (!saved.saved) return c.json({ error: saved.error ?? "Failed to save tunnel state" }, 500);
395
434
  return c.json({
396
435
  ok: true,
397
436
  released
398
437
  });
399
438
  });
400
439
  authenticated.get("/api/tunnel/qr", async (c) => {
401
- await configureTunnelFromService(deps);
402
440
  const port = deps.service.currentConfig.gateway.port ?? 18790;
403
441
  const host = resolveGatewayEffectiveHost(deps.service.currentConfig);
404
442
  if (!requireGatewayToken(c)) return c.json({ error: "Gateway token required" }, 401);
@@ -406,7 +444,6 @@ function registerTunnelRoutes(authenticated, deps) {
406
444
  return c.json(qr);
407
445
  });
408
446
  authenticated.get("/api/tunnel/transport-status", async (c) => {
409
- await configureTunnelFromService(deps);
410
447
  return c.json({ transport: { tls: "broker_terminated" } });
411
448
  });
412
449
  /**
@@ -1,12 +1,8 @@
1
1
  /**
2
2
  * Voice routes — POST /api/voice/transcriptions (multipart).
3
3
  *
4
- * The endpoints share one execution path that:
5
- * 1. Runs the configured STT provider and fallback chain
6
- * 2. Optionally runs LLM refine on the raw transcript
7
- * 3. Returns { raw, refined?, language }
8
- *
9
- * LLM refinement is opt-in through `voice.input.refinement`.
4
+ * Transcription returns the provider text immediately. Optional LLM cleanup
5
+ * uses a separate endpoint so it never delays the raw transcript.
10
6
  */
11
7
  import type { Hono } from 'hono';
12
8
  import type { AuthenticatedRouteDeps } from './deps.js';
@@ -493,14 +493,13 @@ function registerVoiceRoutes(authenticated, deps) {
493
493
  fileName: input.fileName,
494
494
  signal: c.req.raw.signal
495
495
  });
496
- const raw = result.text;
497
496
  const detectedLanguage = result.language ?? input.language;
498
- const refined = raw.trim() ? await refineTranscript(raw, config, c.req.raw.signal) : void 0;
497
+ const refinementAvailable = (config.voice?.input?.refinement?.mode ?? "off") !== "off";
499
498
  return c.json({
500
499
  ok: true,
501
500
  payload: {
502
- raw,
503
- ...refined ? { refined } : {},
501
+ text: result.text,
502
+ refinementAvailable,
504
503
  ...detectedLanguage ? { language: detectedLanguage } : {},
505
504
  provider: result.provider,
506
505
  attempts: result.attempts,
@@ -534,6 +533,23 @@ function registerVoiceRoutes(authenticated, deps) {
534
533
  }, 502);
535
534
  }
536
535
  };
536
+ authenticated.post("/api/voice/transcriptions/refine", strictRateLimitMiddleware, async (c) => {
537
+ const body = await c.req.json().catch(() => null);
538
+ const text = typeof body?.text === "string" ? body.text.trim() : "";
539
+ if (!text) return c.json({
540
+ ok: false,
541
+ error: { message: "Missing transcript text" }
542
+ }, 400);
543
+ if (text.length > 2e4) return c.json({
544
+ ok: false,
545
+ error: { message: "Transcript text exceeds 20000 character limit" }
546
+ }, 413);
547
+ const refined = await refineTranscript(text, service.currentConfig, c.req.raw.signal);
548
+ return c.json({
549
+ ok: true,
550
+ payload: { text: refined ?? text }
551
+ });
552
+ });
537
553
  /** Multipart endpoint used by browser and future native clients. */
538
554
  authenticated.post("/api/voice/transcriptions", strictRateLimitMiddleware, async (c) => {
539
555
  const contentLength = Number(c.req.header("content-length") ?? 0);