@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.
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/feishu/src/outbound/media-load.js +1 -1
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{activity-store-NItGQmvR.js → activity-store--GBciH10.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-avatar-dicebear-DsR_2U9-.js → agent-avatar-dicebear-K7V4XXt7.js} +1 -1
- package/dist/gateway/static/root/assets/agents-BsxTpXyq.js +68 -0
- package/dist/gateway/static/root/assets/{api-53ZXsuyr.js → api-HoIz_ija.js} +1 -1
- package/dist/gateway/static/root/assets/{app-management-settings-panel-BCzVX09V.js → app-management-settings-panel-xQCmkAHh.js} +1 -1
- package/dist/gateway/static/root/assets/{appearance-settings-DgSpwqQI.js → appearance-settings-BRoeA4kU.js} +1 -1
- package/dist/gateway/static/root/assets/apps-page-BXgpBA8w.js +1 -0
- package/dist/gateway/static/root/assets/{archive-plugin-D8o8CP9Y.js → archive-plugin-CteCVsBP.js} +1 -1
- package/dist/gateway/static/root/assets/{attachment-load-BQHcAR6J.js → attachment-load-D7xNA2jB.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-page-CswK3HFb.js → automations-page-BD1vK7r5.js} +1 -1
- package/dist/gateway/static/root/assets/automations-workspace-wrgf-z7D.js +6 -0
- package/dist/gateway/static/root/assets/{binary-plugins-BlpyTJ1b.js → binary-plugins-C3DaJa0W.js} +1 -1
- package/dist/gateway/static/root/assets/block-editor-lTkcHTve.js +184 -0
- package/dist/gateway/static/root/assets/browser-settings-page-zhzNkeVG.js +8 -0
- package/dist/gateway/static/root/assets/{browser-workflow-inputs-Bi6RSSpD.js → browser-workflow-inputs-1UZ0vnIy.js} +1 -1
- package/dist/gateway/static/root/assets/{browser-workflows-page-CZNOOQY4.js → browser-workflows-page-BHuJOF4M.js} +2 -2
- package/dist/gateway/static/root/assets/capabilities-settings-panel-DuCmg2Ej.js +7 -0
- package/dist/gateway/static/root/assets/{capability-presets-api-pC2fno7r.js → capability-presets-api-yNqTIuyd.js} +1 -1
- package/dist/gateway/static/root/assets/capability-presets-settings-panel-DjiP3mQQ.js +3 -0
- package/dist/gateway/static/root/assets/{channel-recipient-api-B5Szgu_z.js → channel-recipient-api-krezGsKc.js} +1 -1
- package/dist/gateway/static/root/assets/channels-settings-B4AtvQHk.js +1 -0
- package/dist/gateway/static/root/assets/channels-status-swr-DMxB7slL.js +1 -0
- package/dist/gateway/static/root/assets/{chat-terminal-dock-2fpzufzt.js → chat-terminal-dock-BRrL2iHA.js} +2 -2
- package/dist/gateway/static/root/assets/connectors-page-fmda2NEP.js +2 -0
- package/dist/gateway/static/root/assets/{cron-expression-VePLfPUR.js → cron-expression-Bl-2DzBQ.js} +1 -1
- package/dist/gateway/static/root/assets/{date-picker-CJegmPTJ.js → date-picker-CxTl6Wlz.js} +1 -1
- package/dist/gateway/static/root/assets/{dependency-picker-B2o9unBk.js → dependency-picker-DSl0mOPa.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-BH4Np-ay.js → desktop-pet-DTeCmCdb.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-settings-Duu3ctxD.js → desktop-pet-settings-BNebxg0V.js} +1 -1
- package/dist/gateway/static/root/assets/{directory-picker-path-field-BBCUZOg2.js → directory-picker-path-field-Dn9XpmXY.js} +1 -1
- package/dist/gateway/static/root/assets/dist-DaxPZj9e.js +1 -0
- package/dist/gateway/static/root/assets/{dist-DY4Af0fY.js → dist-J_ySZvz0.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-DS_o3F97.js → extension-debug-page-D-J-eOTb.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-BTLBZdCq.js → extension-page-CpST24nU.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-KTGrYbgk.js → extension-settings-page-4nQXFnFq.js} +1 -1
- package/dist/gateway/static/root/assets/floating-ui.dom-B7uQOZZW.js +1 -0
- package/dist/gateway/static/root/assets/{gateway-config-api-D3YJVfzY.js → gateway-config-api-BHWHduDi.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-config-swr-BzTbnu4O.js → gateway-config-swr-yUxMMVjK.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-settings-B8--s3yC.js → gateway-settings-BimMKmkM.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-startup-retry-B2zpkNvi.js → gateway-startup-retry-9mrsL84j.js} +1 -1
- package/dist/gateway/static/root/assets/heartbeat-settings-MUJ3mEC9.js +1 -0
- package/dist/gateway/static/root/assets/home-page-DSM6aPHF.js +1 -0
- package/dist/gateway/static/root/assets/{image-generation-api-BXRjz-iY.js → image-generation-api-CgFAGDi8.js} +1 -1
- package/dist/gateway/static/root/assets/index-B3zYJBhK.js +74 -0
- package/dist/gateway/static/root/assets/index-D3_xSYKd.css +2 -0
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-D-Ib-suu.js → keyboard-shortcuts-settings-6Ar5rfX4.js} +1 -1
- package/dist/gateway/static/root/assets/local-app-workbench-page-tzM0wTL8.js +2 -0
- package/dist/gateway/static/root/assets/{local-apps-page-Bsy1lvAc.js → local-apps-page-B5n-x544.js} +1 -1
- package/dist/gateway/static/root/assets/{locale-store-iKjqemWG.js → locale-store-CPylQjH9.js} +1 -1
- package/dist/gateway/static/root/assets/logs-page-CgoQlera.js +2 -0
- package/dist/gateway/static/root/assets/{management-settings-D-akydG4.js → management-settings-CXBLyU-0.js} +1 -1
- package/dist/gateway/static/root/assets/{markdown-split-DHH4Cu2i.js → markdown-split-CtmQemJi.js} +1 -1
- package/dist/gateway/static/root/assets/markdown-view-CUsob4Pz.js +69 -0
- package/dist/gateway/static/root/assets/{media-plugins-BwHtUXkj.js → media-plugins-Dsd-IEOW.js} +1 -1
- package/dist/gateway/static/root/assets/messages-B82nsi9k.js +3 -0
- package/dist/gateway/static/root/assets/notes-api-CfKZYrBC.js +1 -0
- package/dist/gateway/static/root/assets/{notes-page-BxM1p6hz.js → notes-page-BKbOTE2n.js} +1 -1
- package/dist/gateway/static/root/assets/notes-workbench-BScELo01.js +4 -0
- package/dist/gateway/static/root/assets/oauth-authorization-window-Cdolr2wE.js +1 -0
- package/dist/gateway/static/root/assets/{popover-select-BzdnTy3h.js → popover-select-CccGri5Y.js} +1 -1
- package/dist/gateway/static/root/assets/{product-open-page-DrfWH1ir.js → product-open-page-DqB9bsDB.js} +1 -1
- package/dist/gateway/static/root/assets/project-detail-page-DUoyty8X.js +1 -0
- package/dist/gateway/static/root/assets/projects-page-PeXUdGdO.js +1 -0
- package/dist/gateway/static/root/assets/{registry-api-Cx3JxXSL.js → registry-api-B_5D8epF.js} +1 -1
- package/dist/gateway/static/root/assets/remote-access-hub-sHho-uXP.js +1 -0
- package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-DF7syQRd.js → runtime-tools-settings-panel-DBTH_t0O.js} +1 -1
- package/dist/gateway/static/root/assets/{schema-form-BQ_qRIsM.js → schema-form-CqMKAwV0.js} +1 -1
- package/dist/gateway/static/root/assets/sessions-page-DyvNgaP9.js +1 -0
- package/dist/gateway/static/root/assets/settings-advanced-gate-CnYf1R7z.js +1 -0
- package/dist/gateway/static/root/assets/{settings-form-section-BBHGOQHU.js → settings-form-section-dPriuuYq.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-loading-skeleton-CiwjP2-b.js → settings-loading-skeleton-B-Mbm8Va.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-Rl9waapk.js → settings-page-Dyj3w-Ml.js} +1 -1
- package/dist/gateway/static/root/assets/setup-status-panel-C4FxY5J3.js +1 -0
- package/dist/gateway/static/root/assets/share-preview-page-emEdx7YL.js +2 -0
- package/dist/gateway/static/root/assets/shares-settings-BQBVq-sN.js +1 -0
- package/dist/gateway/static/root/assets/skill-reload-api-BFoEpQ4I.js +1 -0
- package/dist/gateway/static/root/assets/{skills-page-YbtR_zEU.js → skills-page-BNtYnrRT.js} +1 -1
- package/dist/gateway/static/root/assets/{skills-page.utils-DTmx2Zza.js → skills-page.utils-DjnbNFs5.js} +1 -1
- package/dist/gateway/static/root/assets/{system-settings-panel-BZDJcKdS.js → system-settings-panel-DDStf-jZ.js} +1 -1
- package/dist/gateway/static/root/assets/task-detail-page-ZVwr8KbS.js +1 -0
- package/dist/gateway/static/root/assets/{theme-store-BOD2D0dJ.js → theme-store-BN-32Fz5.js} +1 -1
- package/dist/gateway/static/root/assets/{use-autosave-Be9aR9qA.js → use-autosave-C2mKZ-ua.js} +1 -1
- package/dist/gateway/static/root/assets/user-context-page-CHjIinCW.js +1 -0
- package/dist/gateway/static/root/assets/{voice-api-key-field-IMc3Ua6C.js → voice-api-key-field-D7OHeQby.js} +1 -1
- package/dist/gateway/static/root/assets/{work-discovery-overlay-TC-xQenr.js → work-discovery-overlay-CIiXie7F.js} +1 -1
- package/dist/gateway/static/root/assets/{workflow-run-setup-panel-B2auxcRv.js → workflow-run-setup-panel-9_dAGW3_.js} +2 -2
- package/dist/gateway/static/root/assets/workflows-page-DXpYJxao.js +5 -0
- package/dist/gateway/static/root/index.html +15 -14
- package/dist/package.js +1 -1
- package/dist/src/agent/agent-instance-gateway.d.ts +3 -0
- package/dist/src/agent/agent-manager.d.ts +3 -0
- package/dist/src/agent/agent-manager.js +30 -24
- package/dist/src/agent/embedded/index.d.ts +1 -1
- package/dist/src/agent/embedded/index.js +2 -2
- package/dist/src/agent/embedded/run-for-session.js +101 -62
- package/dist/src/agent/embedded/run-turn.js +56 -28
- package/dist/src/agent/embedded/runs.d.ts +16 -29
- package/dist/src/agent/embedded/runs.js +44 -65
- package/dist/src/agent/embedded/types.d.ts +4 -0
- package/dist/src/agent/orchestration/agent-run-supervisor.d.ts +28 -0
- package/dist/src/agent/orchestration/agent-run-supervisor.js +49 -0
- package/dist/src/agent/orchestration/agent-turn-policy.d.ts +18 -0
- package/dist/src/agent/orchestration/agent-turn-policy.js +36 -0
- package/dist/src/agent/orchestration/llm-turn-retry.d.ts +5 -0
- package/dist/src/agent/orchestration/llm-turn-retry.js +34 -4
- package/dist/src/agent/orchestration/run-agent-turn-with-timeout.d.ts +10 -1
- package/dist/src/agent/orchestration/run-agent-turn-with-timeout.js +25 -4
- package/dist/src/agent/service/async-queue.d.ts +16 -3
- package/dist/src/agent/service/async-queue.js +29 -11
- package/dist/src/agent/service/process-direct-streaming.js +27 -4
- package/dist/src/agent/service.js +1 -0
- package/dist/src/agent/tools/exec-command.d.ts +2 -0
- package/dist/src/agent/tools/exec-command.js +70 -7
- package/dist/src/auth/oauth/index.js +3 -3
- package/dist/src/auth/oauth/registry.d.ts +2 -0
- package/dist/src/auth/oauth/registry.js +20 -12
- package/dist/src/auth/oauth/xopc-cloud.d.ts +1 -0
- package/dist/src/auth/oauth/xopc-cloud.js +32 -16
- package/dist/src/cli/commands/channels.js +1 -1
- package/dist/src/cli/commands/connectors.js +1 -1
- package/dist/src/cli/commands/extension-marketplace.js +1 -1
- package/dist/src/cli/commands/models.js +1 -1
- package/dist/src/cli/commands/onboard.js +1 -1
- package/dist/src/cli/commands/tunnel.js +4 -5
- package/dist/src/config/agent-profile.js +1 -1
- package/dist/src/config/schema.d.ts +18 -0
- package/dist/src/config/schema.js +19 -1
- package/dist/src/extensions/sdk/index.js +1 -1
- package/dist/src/gateway/agents-admin.js +1 -1
- package/dist/src/gateway/hono/app.js +2 -1
- package/dist/src/gateway/hono/oauth-async.d.ts +1 -0
- package/dist/src/gateway/hono/oauth-async.js +11 -4
- package/dist/src/gateway/hono/routes/config.js +10 -0
- package/dist/src/gateway/hono/routes/host-fs.js +40 -1
- package/dist/src/gateway/hono/routes/media.js +33 -1
- package/dist/src/gateway/hono/routes/notes.js +162 -7
- package/dist/src/gateway/hono/routes/shares.js +141 -6
- package/dist/src/gateway/hono/routes/tunnel.js +47 -10
- package/dist/src/gateway/hono/routes/voice.d.ts +2 -6
- package/dist/src/gateway/hono/routes/voice.js +20 -4
- package/dist/src/gateway/hono/routes/workflows.js +2 -2
- package/dist/src/gateway/run-loop.js +3 -0
- package/dist/src/gateway/server.d.ts +3 -0
- package/dist/src/gateway/server.js +30 -5
- package/dist/src/gateway/service.js +4 -4
- package/dist/src/notes/service.d.ts +6 -2
- package/dist/src/notes/service.js +36 -4
- package/dist/src/providers/index.js +2 -2
- package/dist/src/share/note-share-service.d.ts +67 -0
- package/dist/src/share/note-share-service.js +283 -0
- package/dist/src/share/share-config.js +32 -3
- package/dist/src/share/share-landing.d.ts +5 -1
- package/dist/src/share/share-landing.js +28 -1
- package/dist/src/share/share-store.d.ts +34 -5
- package/dist/src/share/share-store.js +125 -19
- package/dist/src/share/share-thumbnail.js +4 -2
- package/dist/src/share/share-types.d.ts +52 -14
- package/dist/src/share/share-types.js +9 -0
- package/dist/src/share/share-zip.d.ts +2 -2
- package/dist/src/tui/backends/embedded-backend.js +1 -1
- package/dist/src/tunnel/env.d.ts +12 -6
- package/dist/src/tunnel/env.js +34 -26
- package/dist/src/tunnel/gateway-lifecycle.js +2 -13
- package/dist/src/tunnel/index.d.ts +1 -1
- package/dist/src/tunnel/index.js +2 -2
- package/dist/src/tunnel/tunnel-audit.d.ts +1 -1
- package/dist/src/tunnel/tunnel-config.d.ts +1 -1
- package/dist/src/tunnel/tunnel-config.js +9 -4
- package/dist/src/tunnel/tunnel-service.d.ts +1 -1
- package/dist/src/tunnel/tunnel-service.js +3 -0
- package/dist/src/tunnel/well-known.d.ts +1 -1
- package/dist/src/tunnel/well-known.js +1 -1
- package/dist/src/tunnel/xopc-cloud-registration.d.ts +9 -0
- package/dist/src/tunnel/xopc-cloud-registration.js +42 -0
- package/dist/src/voice/stt/transcribe-core.js +17 -1
- package/dist/src/voice/stt/types.d.ts +1 -1
- package/package.json +2 -2
- package/dist/gateway/static/root/assets/agents-DijFbC4G.js +0 -68
- package/dist/gateway/static/root/assets/apps-page-BFAmf9hq.js +0 -1
- package/dist/gateway/static/root/assets/automations-workspace-B8us5JBf.js +0 -6
- package/dist/gateway/static/root/assets/block-editor-C3mwGsms.js +0 -184
- package/dist/gateway/static/root/assets/browser-settings-page-BeBd17-3.js +0 -8
- package/dist/gateway/static/root/assets/capabilities-settings-panel-Bv0z3KRq.js +0 -7
- package/dist/gateway/static/root/assets/capability-presets-settings-panel-BczD-pWE.js +0 -3
- package/dist/gateway/static/root/assets/channels-settings-2KQYq6xl.js +0 -1
- package/dist/gateway/static/root/assets/channels-status-swr-ByRbxC6z.js +0 -1
- package/dist/gateway/static/root/assets/connectors-page-PB1boaNy.js +0 -2
- package/dist/gateway/static/root/assets/dist-C230izr8.js +0 -1
- package/dist/gateway/static/root/assets/heartbeat-settings-5YQ5BUwp.js +0 -1
- package/dist/gateway/static/root/assets/home-page-DfIVWJl3.js +0 -1
- package/dist/gateway/static/root/assets/index-BXGC3VFm.css +0 -2
- package/dist/gateway/static/root/assets/index-jqDCfet4.js +0 -74
- package/dist/gateway/static/root/assets/local-app-workbench-page-CAb0odzu.js +0 -2
- package/dist/gateway/static/root/assets/logs-page-CFxKbDZl.js +0 -2
- package/dist/gateway/static/root/assets/markdown-view-DYkxMtJA.js +0 -69
- package/dist/gateway/static/root/assets/messages-kx5W39dO.js +0 -3
- package/dist/gateway/static/root/assets/notes-api-DJizatxh.js +0 -1
- package/dist/gateway/static/root/assets/notes-workbench-BGjEC89c.js +0 -4
- package/dist/gateway/static/root/assets/project-detail-page-Dj9kEQwg.js +0 -1
- package/dist/gateway/static/root/assets/projects-page-CBRy5Ezz.js +0 -1
- package/dist/gateway/static/root/assets/remote-access-hub-DSwz9RBh.js +0 -1
- package/dist/gateway/static/root/assets/sessions-page-WtattyVX.js +0 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-CNAz_q1F.js +0 -1
- package/dist/gateway/static/root/assets/setup-status-panel-CNo4Ajhn.js +0 -1
- package/dist/gateway/static/root/assets/share-preview-page-B7PI3yZ6.js +0 -2
- package/dist/gateway/static/root/assets/shares-settings-DpOH0zWB.js +0 -1
- package/dist/gateway/static/root/assets/skill-reload-api-BOf4xCat.js +0 -1
- package/dist/gateway/static/root/assets/task-detail-page-DrMX5Yhj.js +0 -1
- package/dist/gateway/static/root/assets/user-context-page-ewIK2ddC.js +0 -1
- 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")
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
...
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
|
497
|
+
const refinementAvailable = (config.voice?.input?.refinement?.mode ?? "off") !== "off";
|
|
499
498
|
return c.json({
|
|
500
499
|
ok: true,
|
|
501
500
|
payload: {
|
|
502
|
-
|
|
503
|
-
|
|
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);
|