ocean-brain 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/server/client/dist/assets/ArrowClockwise.es-BwtKWcmj.js +1 -0
- package/server/client/dist/assets/{ArrowRight.es-nBJws3ws.js → ArrowRight.es-E09Lfefr.js} +1 -1
- package/server/client/dist/assets/{AuxiliaryPanelHeader-BN_2axQu.js → AuxiliaryPanelHeader-Bys98BJJ.js} +1 -1
- package/server/client/dist/assets/Calendar-BOhJYi8K.js +25 -0
- package/server/client/dist/assets/Callout-CAH1ua9B.js +1 -0
- package/server/client/dist/assets/{CaretUp.es-DDbqKn78.js → CaretUp.es-DSWjIa-J.js} +1 -1
- package/server/client/dist/assets/{CheckCircle.es-CyY9aLBF.js → CheckCircle.es-Cou6rItm.js} +1 -1
- package/server/client/dist/assets/{Copy.es-BwX4tEoU.js → Copy.es-ftEldGIL.js} +1 -1
- package/server/client/dist/assets/{File.es-DeTgvUnI.js → File.es-DlKAX1uO.js} +1 -1
- package/server/client/dist/assets/{FileText.es-C-bpw8dQ.js → FileText.es-D6JM_MqM.js} +1 -1
- package/server/client/dist/assets/{FlowArrow.es-CKcMBOmX.js → FlowArrow.es-BRWXqf9A.js} +1 -1
- package/server/client/dist/assets/{Graph-BNoSRJP3.js → Graph-BaqGi3M7.js} +1 -1
- package/server/client/dist/assets/{Graph-DYYxNAWn.js → Graph-L-d0cdMk.js} +1 -1
- package/server/client/dist/assets/{Image.es-d4jWnijM.js → Image.es-Bh2mTNtr.js} +1 -1
- package/server/client/dist/assets/{Info.es-DVzN2joA.js → Info.es-Bwz4CSvS.js} +1 -1
- package/server/client/dist/assets/Integration-C-rva2X9.js +1 -0
- package/server/client/dist/assets/{LinkSimple.es-BD4aGTc3.js → LinkSimple.es-iJvLf-MB.js} +1 -1
- package/server/client/dist/assets/{Note-FL31jb2U.js → Note-OO_uh2a7.js} +4 -4
- package/server/client/dist/assets/{NotePropertiesPanel-os22Ppnu.js → NotePropertiesPanel-BFWjxL_w.js} +1 -1
- package/server/client/dist/assets/{Plus.es-Cl8zOOZa.js → Plus.es-Dmb3oijD.js} +1 -1
- package/server/client/dist/assets/Reminders-CtpFtvu6.js +1 -0
- package/server/client/dist/assets/{Search-CPPTW2PO.js → Search-t48pkUg-.js} +2 -2
- package/server/client/dist/assets/{SurfaceCard-jL2qzrzF.js → SurfaceCard-zdJ8k1o0.js} +1 -1
- package/server/client/dist/assets/{Tag-3DbGmuxa.js → Tag-BL2h3ZaX.js} +1 -1
- package/server/client/dist/assets/TagNotes-BpRTCEBf.js +1 -0
- package/server/client/dist/assets/{Trash.es-fxukjAO6.js → Trash.es-DFnO8iF5.js} +1 -1
- package/server/client/dist/assets/{ViewChip-CVe_HNyd.js → ViewChip-BdPhSEvG.js} +1 -1
- package/server/client/dist/assets/Views-BoO6vgnF.js +1 -0
- package/server/client/dist/assets/WarningCircle.es-BAvgGuQK.js +1 -0
- package/server/client/dist/assets/app-OGsLwSqJ.js +151 -0
- package/server/client/dist/assets/app-yYMsgUrz.css +1 -0
- package/server/client/dist/assets/{calendar-data-BQ24n348.js → calendar-data-CwVF7Pz3.js} +1 -1
- package/server/client/dist/assets/{image.api-CyIIEGkG.js → image.api-CHuhmq_G.js} +1 -1
- package/server/client/dist/assets/index-CuCzOHJ6.js +1 -0
- package/server/client/dist/assets/integrations-B5SIEnG_.js +1 -0
- package/server/client/dist/assets/{manage-image-DXedfr7U.js → manage-image-Kd-7IGfB.js} +1 -1
- package/server/client/dist/assets/{manage-image-detail-DmifOPNU.js → manage-image-detail-HU0QLP-t.js} +1 -1
- package/server/client/dist/assets/mcp-BQ418pyx.js +5 -0
- package/server/client/dist/assets/{mcp-admin-adapter-CmKsg1vd.js → mcp-admin-adapter-CUakQ_Xm.js} +1 -1
- package/server/client/dist/assets/{note-ui-DRnAnwKv.js → note-ui-CRcP5BjO.js} +1 -1
- package/server/client/dist/assets/{placeholder-G6iEBbDy.js → placeholder-D7JKnpuh.js} +1 -1
- package/server/client/dist/assets/{properties-BPobx_Qk.js → properties-CtagYHMr.js} +1 -1
- package/server/client/dist/assets/route-preload.js +2 -2
- package/server/client/dist/assets/search-B5tCkhwj.js +1 -0
- package/server/client/dist/assets/{search-admin-adapter-7sTcWTmv.js → search-admin-adapter-Be9xSY88.js} +1 -1
- package/server/client/dist/assets/{tag.api-kKZl04yY.js → tag.api-DGfi8hWL.js} +1 -1
- package/server/client/dist/assets/{time-BwwQGbK-.js → time-Kh6oav5R.js} +9 -9
- package/server/client/dist/assets/{trash-Cepvi7G8.js → trash-Di2BWyUO.js} +1 -1
- package/server/client/dist/assets/{useNoteMutate-D-mZKXjl.js → useNoteMutate-BQj6jHhV.js} +1 -1
- package/server/client/dist/assets/{useReminderMutate-Cexk1ydX.js → useReminderMutate-jubqwSD9.js} +1 -1
- package/server/client/dist/index.html +5 -5
- package/server/dist/app.js +9 -1
- package/server/dist/features/app-gateway/access.js +73 -0
- package/server/dist/features/app-gateway/database.js +20 -0
- package/server/dist/features/app-gateway/gateway.js +208 -0
- package/server/dist/features/integration/access-events.js +17 -0
- package/server/dist/features/integration/manifest.js +31 -2
- package/server/dist/features/integration/note-sync.js +156 -0
- package/server/dist/features/integration/service.js +80 -7
- package/server/dist/features/integration/status-report.js +22 -0
- package/server/dist/features/note/graphql/note.mutation.resolver.js +20 -12
- package/server/dist/features/note/graphql/note.query.resolver.js +4 -1
- package/server/dist/features/note/services/change-events.js +26 -0
- package/server/dist/features/search/search-manager.js +4 -7
- package/server/dist/modules/server-events.js +3 -0
- package/server/dist/routes/apps.js +163 -0
- package/server/dist/routes/index.js +2 -0
- package/server/dist/routes/integrations.js +21 -1
- package/server/prisma/migrations/20260919090000_0022_integration_proxy_url/migration.sql +2 -0
- package/server/prisma/schema.prisma +2 -0
- package/server/client/dist/assets/Calendar-vA4Xa4Qr.js +0 -25
- package/server/client/dist/assets/Callout-Bo2P960P.js +0 -1
- package/server/client/dist/assets/Integration-gDDtIJYJ.js +0 -1
- package/server/client/dist/assets/Reminders-Dt2JuGci.js +0 -1
- package/server/client/dist/assets/TagNotes-DLwRF8pT.js +0 -1
- package/server/client/dist/assets/Views-8ghLRax7.js +0 -1
- package/server/client/dist/assets/app-Bcl0U7r9.css +0 -1
- package/server/client/dist/assets/app-DzChh-V0.js +0 -151
- package/server/client/dist/assets/index-Iy_BgP1R.js +0 -1
- package/server/client/dist/assets/integrations-LOdTs1T6.js +0 -1
- package/server/client/dist/assets/mcp-d8IzsoRz.js +0 -5
- package/server/client/dist/assets/search-DanMKCC8.js +0 -1
- package/server/dist/features/search/note-change.js +0 -16
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { subscribeNoteChanges } from "../../features/note/services/change-events.js";
|
|
3
|
+
import models from "../../models.js";
|
|
4
|
+
import { createAppError } from "../../modules/error-handler.js";
|
|
5
|
+
import { subscribeIntegrationAccessChanges } from "./access-events.js";
|
|
6
|
+
const DEFAULT_PAGE_SIZE = 100;
|
|
7
|
+
const MAX_PAGE_SIZE = 500;
|
|
8
|
+
const KEEP_ALIVE_INTERVAL_MS = 3e4;
|
|
9
|
+
const MAX_STREAMS_PER_CONNECTION = 2;
|
|
10
|
+
const MAX_NOTE_ID = 2147483647;
|
|
11
|
+
const activeStreamsByConnection = /* @__PURE__ */ new Map();
|
|
12
|
+
const resolvePageSize = (value) => {
|
|
13
|
+
if (value === void 0) return DEFAULT_PAGE_SIZE;
|
|
14
|
+
if (!Number.isInteger(value) || Number(value) < 1 || Number(value) > MAX_PAGE_SIZE) {
|
|
15
|
+
throw createAppError(
|
|
16
|
+
400,
|
|
17
|
+
"INVALID_NOTE_CATALOG_PAGE_SIZE",
|
|
18
|
+
`limit must be an integer between 1 and ${MAX_PAGE_SIZE}.`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return Number(value);
|
|
22
|
+
};
|
|
23
|
+
const resolveAfterId = (value) => {
|
|
24
|
+
if (value === void 0) return 0;
|
|
25
|
+
const normalized = typeof value === "number" && Number.isSafeInteger(value) ? String(value) : value;
|
|
26
|
+
if (typeof normalized !== "string" || !/^(0|[1-9]\d*)$/.test(normalized)) {
|
|
27
|
+
throw createAppError(400, "INVALID_NOTE_CATALOG_CURSOR", "afterId must be a nonnegative integer.");
|
|
28
|
+
}
|
|
29
|
+
const parsed = Number(normalized);
|
|
30
|
+
if (!Number.isSafeInteger(parsed) || parsed > MAX_NOTE_ID) {
|
|
31
|
+
throw createAppError(400, "INVALID_NOTE_CATALOG_CURSOR", "afterId exceeds the supported range.");
|
|
32
|
+
}
|
|
33
|
+
return parsed;
|
|
34
|
+
};
|
|
35
|
+
const createIntegrationNoteCatalogHandler = () => {
|
|
36
|
+
return async (request, reply) => {
|
|
37
|
+
const limit = resolvePageSize(request.body?.limit);
|
|
38
|
+
const afterId = resolveAfterId(request.body?.afterId);
|
|
39
|
+
const [rows, propertySchema] = await Promise.all([
|
|
40
|
+
models.note.findMany({
|
|
41
|
+
where: { id: { gt: afterId } },
|
|
42
|
+
orderBy: { id: "asc" },
|
|
43
|
+
take: limit + 1,
|
|
44
|
+
select: { id: true, updatedAt: true }
|
|
45
|
+
}),
|
|
46
|
+
afterId === 0 ? models.propertyDefinition.findMany({
|
|
47
|
+
orderBy: { key: "asc" },
|
|
48
|
+
select: {
|
|
49
|
+
key: true,
|
|
50
|
+
name: true,
|
|
51
|
+
valueType: true,
|
|
52
|
+
updatedAt: true,
|
|
53
|
+
options: {
|
|
54
|
+
orderBy: [{ order: "asc" }, { id: "asc" }],
|
|
55
|
+
select: {
|
|
56
|
+
id: true,
|
|
57
|
+
label: true,
|
|
58
|
+
value: true,
|
|
59
|
+
color: true,
|
|
60
|
+
order: true,
|
|
61
|
+
updatedAt: true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}) : Promise.resolve(null)
|
|
66
|
+
]);
|
|
67
|
+
const hasMore = rows.length > limit;
|
|
68
|
+
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
69
|
+
return reply.status(200).send({
|
|
70
|
+
notes: page.map((note) => ({ id: String(note.id), updatedAt: note.updatedAt.toISOString() })),
|
|
71
|
+
...propertySchema ? { propertySchemaHash: createHash("sha256").update(JSON.stringify(propertySchema)).digest("hex") } : {},
|
|
72
|
+
hasMore,
|
|
73
|
+
nextAfterId: hasMore ? String(page.at(-1)?.id) : null
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
const serializeIntegrationNoteEvent = (event) => {
|
|
78
|
+
const data = {
|
|
79
|
+
type: event.type,
|
|
80
|
+
noteId: String(event.noteId),
|
|
81
|
+
occurredAt: event.occurredAt
|
|
82
|
+
};
|
|
83
|
+
return `event: ${event.type}
|
|
84
|
+
data: ${JSON.stringify(data)}
|
|
85
|
+
|
|
86
|
+
`;
|
|
87
|
+
};
|
|
88
|
+
const createIntegrationNoteEventsHandler = (subscribe = subscribeNoteChanges) => {
|
|
89
|
+
return async (request, reply) => {
|
|
90
|
+
const connectionId = request.integration?.connectionId;
|
|
91
|
+
if (!connectionId) {
|
|
92
|
+
throw createAppError(401, "UNAUTHORIZED", "An authenticated integration connection is required.");
|
|
93
|
+
}
|
|
94
|
+
const activeStreamCount = activeStreamsByConnection.get(connectionId) ?? 0;
|
|
95
|
+
if (activeStreamCount >= MAX_STREAMS_PER_CONNECTION) {
|
|
96
|
+
throw createAppError(
|
|
97
|
+
429,
|
|
98
|
+
"INTEGRATION_EVENT_STREAM_LIMIT",
|
|
99
|
+
`A connection can open at most ${MAX_STREAMS_PER_CONNECTION} event streams.`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
reply.hijack();
|
|
103
|
+
const response = reply.raw;
|
|
104
|
+
response.statusCode = 200;
|
|
105
|
+
response.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
106
|
+
response.setHeader("Cache-Control", "no-cache, no-transform");
|
|
107
|
+
response.setHeader("Connection", "keep-alive");
|
|
108
|
+
response.setHeader("X-Accel-Buffering", "no");
|
|
109
|
+
response.flushHeaders();
|
|
110
|
+
response.write(": connected\n\n");
|
|
111
|
+
activeStreamsByConnection.set(connectionId, activeStreamCount + 1);
|
|
112
|
+
let cleanedUp = false;
|
|
113
|
+
let keepAliveTimer = null;
|
|
114
|
+
let unsubscribeNoteChanges = () => void 0;
|
|
115
|
+
let unsubscribeAccessChanges = () => void 0;
|
|
116
|
+
const cleanup = () => {
|
|
117
|
+
if (cleanedUp) return;
|
|
118
|
+
cleanedUp = true;
|
|
119
|
+
if (keepAliveTimer) clearInterval(keepAliveTimer);
|
|
120
|
+
unsubscribeNoteChanges();
|
|
121
|
+
unsubscribeAccessChanges();
|
|
122
|
+
const remainingStreams = (activeStreamsByConnection.get(connectionId) ?? 1) - 1;
|
|
123
|
+
if (remainingStreams > 0) activeStreamsByConnection.set(connectionId, remainingStreams);
|
|
124
|
+
else activeStreamsByConnection.delete(connectionId);
|
|
125
|
+
};
|
|
126
|
+
const endStream = () => {
|
|
127
|
+
cleanup();
|
|
128
|
+
if (!response.writableEnded) {
|
|
129
|
+
try {
|
|
130
|
+
response.end();
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const write = (chunk) => {
|
|
136
|
+
try {
|
|
137
|
+
if (!response.write(chunk)) endStream();
|
|
138
|
+
} catch {
|
|
139
|
+
endStream();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
unsubscribeNoteChanges = subscribe((event) => write(serializeIntegrationNoteEvent(event)));
|
|
143
|
+
unsubscribeAccessChanges = subscribeIntegrationAccessChanges((changedConnectionId) => {
|
|
144
|
+
if (changedConnectionId === connectionId) endStream();
|
|
145
|
+
});
|
|
146
|
+
keepAliveTimer = setInterval(() => write(": keepalive\n\n"), KEEP_ALIVE_INTERVAL_MS);
|
|
147
|
+
keepAliveTimer.unref?.();
|
|
148
|
+
request.raw.on("close", cleanup);
|
|
149
|
+
response.on("close", cleanup);
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
export {
|
|
153
|
+
createIntegrationNoteCatalogHandler,
|
|
154
|
+
createIntegrationNoteEventsHandler,
|
|
155
|
+
serializeIntegrationNoteEvent
|
|
156
|
+
};
|
|
@@ -2,13 +2,16 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import models from "../../models.js";
|
|
3
3
|
import { createAppError } from "../../modules/error-handler.js";
|
|
4
4
|
import { issueIntegrationToken } from "../../modules/integration-token.js";
|
|
5
|
+
import { emitIntegrationAccessChanged } from "./access-events.js";
|
|
5
6
|
import {
|
|
6
7
|
MCP_CONNECTION_ID,
|
|
7
8
|
NATIVE_INTEGRATIONS,
|
|
9
|
+
parseIntegrationProxyUrl,
|
|
8
10
|
parseManifest,
|
|
9
11
|
parsePermissions,
|
|
10
12
|
validateGrants
|
|
11
13
|
} from "./manifest.js";
|
|
14
|
+
import { parseStatusUpdate, readStatusReport } from "./status-report.js";
|
|
12
15
|
const resolveManifest = (row) => {
|
|
13
16
|
if (row.native) {
|
|
14
17
|
const definition = NATIVE_INTEGRATIONS.find((integration) => integration.connectionId === row.id);
|
|
@@ -20,18 +23,31 @@ const toConnection = (row) => ({
|
|
|
20
23
|
id: row.id,
|
|
21
24
|
native: row.native,
|
|
22
25
|
manifest: resolveManifest(row),
|
|
26
|
+
proxyConfigured: row.proxyUrl !== null,
|
|
23
27
|
grantedPermissions: parsePermissions(JSON.parse(row.grantedPermissions)),
|
|
24
28
|
enabled: row.enabled,
|
|
25
29
|
pinned: row.pinned,
|
|
26
30
|
createdAt: row.createdAt.toISOString(),
|
|
27
31
|
updatedAt: row.updatedAt.toISOString(),
|
|
32
|
+
statusReport: readStatusReport(row.credential?.statusReport ?? null),
|
|
28
33
|
token: row.credential ? {
|
|
29
34
|
id: row.credential.id,
|
|
30
35
|
createdAt: row.credential.createdAt.toISOString(),
|
|
31
36
|
lastUsedAt: row.credential.lastUsedAt?.toISOString() ?? null
|
|
32
37
|
} : null
|
|
33
38
|
});
|
|
34
|
-
const credentialSummary = { select: { id: true, createdAt: true, lastUsedAt: true } };
|
|
39
|
+
const credentialSummary = { select: { id: true, createdAt: true, lastUsedAt: true, statusReport: true } };
|
|
40
|
+
const parseConnectionProxyUrl = (manifest, value) => {
|
|
41
|
+
if (manifest.launch?.mode === "proxied") return parseIntegrationProxyUrl(value);
|
|
42
|
+
if (value !== void 0) {
|
|
43
|
+
throw createAppError(
|
|
44
|
+
400,
|
|
45
|
+
"UNEXPECTED_INTEGRATION_PROXY_URL",
|
|
46
|
+
"A private app URL can only be configured for proxied launches."
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
35
51
|
const createIntegrationService = (db = models) => {
|
|
36
52
|
const get = async (id) => {
|
|
37
53
|
const row = await db.integrationConnection.findUnique({
|
|
@@ -75,10 +91,12 @@ const createIntegrationService = (db = models) => {
|
|
|
75
91
|
"The ocean-brain namespace is reserved for native integrations."
|
|
76
92
|
);
|
|
77
93
|
const grantedPermissions = validateGrants(input.grantedPermissions ?? [], manifest);
|
|
94
|
+
const proxyUrl = parseConnectionProxyUrl(manifest, input.proxyUrl);
|
|
78
95
|
const row = await db.integrationConnection.create({
|
|
79
96
|
data: {
|
|
80
97
|
integrationId: manifest.id,
|
|
81
98
|
manifest: JSON.stringify(manifest),
|
|
99
|
+
proxyUrl,
|
|
82
100
|
grantedPermissions: JSON.stringify(grantedPermissions)
|
|
83
101
|
},
|
|
84
102
|
include: { credential: credentialSummary }
|
|
@@ -86,7 +104,7 @@ const createIntegrationService = (db = models) => {
|
|
|
86
104
|
return toConnection(row);
|
|
87
105
|
},
|
|
88
106
|
async update(id, input) {
|
|
89
|
-
|
|
107
|
+
const connection = await db.$transaction(async (tx) => {
|
|
90
108
|
const current = await tx.integrationConnection.findUnique({ where: { id } });
|
|
91
109
|
if (!current)
|
|
92
110
|
throw createAppError(404, "INTEGRATION_NOT_FOUND", "The integration connection was not found.");
|
|
@@ -96,21 +114,50 @@ const createIntegrationService = (db = models) => {
|
|
|
96
114
|
"NATIVE_INTEGRATION_MANIFEST",
|
|
97
115
|
"Native integration manifests are managed by Ocean Brain."
|
|
98
116
|
);
|
|
99
|
-
const
|
|
117
|
+
for (const key of ["enabled", "pinned"]) {
|
|
118
|
+
if (input[key] !== void 0 && typeof input[key] !== "boolean")
|
|
119
|
+
throw createAppError(400, "INVALID_INTEGRATION_SETTING", `${key} must be a boolean.`);
|
|
120
|
+
}
|
|
121
|
+
const currentManifest = resolveManifest(current);
|
|
122
|
+
const manifest = input.manifest === void 0 ? currentManifest : parseManifest(input.manifest);
|
|
100
123
|
if (manifest.id !== current.integrationId)
|
|
101
124
|
throw createAppError(400, "INTEGRATION_ID_CHANGED", "An upgrade cannot change the integration id.");
|
|
102
125
|
const previousGrants = parsePermissions(JSON.parse(current.grantedPermissions)).filter(
|
|
103
126
|
(permission) => manifest.permissions.includes(permission)
|
|
104
127
|
);
|
|
105
128
|
const grantedPermissions = validateGrants(input.grantedPermissions ?? previousGrants, manifest);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
129
|
+
let proxyUrl = current.proxyUrl;
|
|
130
|
+
if (manifest.launch?.mode === "proxied") {
|
|
131
|
+
if (input.proxyUrl !== void 0) proxyUrl = parseIntegrationProxyUrl(input.proxyUrl);
|
|
132
|
+
const enteringProxiedMode = input.manifest !== void 0 && currentManifest.launch?.mode !== "proxied";
|
|
133
|
+
if ((enteringProxiedMode || input.enabled === true) && !proxyUrl) {
|
|
134
|
+
throw createAppError(
|
|
135
|
+
400,
|
|
136
|
+
"INTEGRATION_PROXY_URL_REQUIRED",
|
|
137
|
+
"Configure a private app URL before enabling a proxied integration."
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
if (input.proxyUrl !== void 0) {
|
|
142
|
+
throw createAppError(
|
|
143
|
+
400,
|
|
144
|
+
"UNEXPECTED_INTEGRATION_PROXY_URL",
|
|
145
|
+
"A private app URL can only be configured for proxied launches."
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
proxyUrl = null;
|
|
149
|
+
}
|
|
150
|
+
if (input.enabled !== void 0 || input.grantedPermissions !== void 0 || input.manifest !== void 0 || input.proxyUrl !== void 0) {
|
|
151
|
+
await tx.integrationCredential.updateMany({
|
|
152
|
+
where: { connectionId: id },
|
|
153
|
+
data: { statusReport: null }
|
|
154
|
+
});
|
|
109
155
|
}
|
|
110
156
|
const row = await tx.integrationConnection.update({
|
|
111
157
|
where: { id },
|
|
112
158
|
data: {
|
|
113
159
|
manifest: JSON.stringify(manifest),
|
|
160
|
+
proxyUrl,
|
|
114
161
|
grantedPermissions: JSON.stringify(grantedPermissions),
|
|
115
162
|
...typeof input.enabled === "boolean" ? { enabled: input.enabled } : {},
|
|
116
163
|
...typeof input.pinned === "boolean" ? { pinned: input.pinned } : {}
|
|
@@ -119,6 +166,10 @@ const createIntegrationService = (db = models) => {
|
|
|
119
166
|
});
|
|
120
167
|
return toConnection(row);
|
|
121
168
|
});
|
|
169
|
+
if (input.enabled !== void 0 || input.grantedPermissions !== void 0 || input.manifest !== void 0) {
|
|
170
|
+
emitIntegrationAccessChanged(id);
|
|
171
|
+
}
|
|
172
|
+
return connection;
|
|
122
173
|
},
|
|
123
174
|
async disconnect(id) {
|
|
124
175
|
const connection = await get(id);
|
|
@@ -129,6 +180,7 @@ const createIntegrationService = (db = models) => {
|
|
|
129
180
|
"Native integrations can be disabled, but cannot be disconnected."
|
|
130
181
|
);
|
|
131
182
|
await db.integrationConnection.delete({ where: { id } });
|
|
183
|
+
emitIntegrationAccessChanged(id);
|
|
132
184
|
},
|
|
133
185
|
async rotateToken(id) {
|
|
134
186
|
await get(id);
|
|
@@ -136,13 +188,34 @@ const createIntegrationService = (db = models) => {
|
|
|
136
188
|
await db.integrationCredential.upsert({
|
|
137
189
|
where: { connectionId: id },
|
|
138
190
|
create: { connectionId: id, tokenHash: token.hash },
|
|
139
|
-
update: { tokenHash: token.hash, createdAt: /* @__PURE__ */ new Date(), lastUsedAt: null }
|
|
191
|
+
update: { tokenHash: token.hash, createdAt: /* @__PURE__ */ new Date(), lastUsedAt: null, statusReport: null }
|
|
140
192
|
});
|
|
193
|
+
emitIntegrationAccessChanged(id);
|
|
141
194
|
return { token: token.plaintext };
|
|
142
195
|
},
|
|
143
196
|
async revokeToken(id) {
|
|
144
197
|
await get(id);
|
|
145
198
|
await db.integrationCredential.deleteMany({ where: { connectionId: id } });
|
|
199
|
+
emitIntegrationAccessChanged(id);
|
|
200
|
+
},
|
|
201
|
+
async reportStatus(token, input) {
|
|
202
|
+
const update = parseStatusUpdate(input);
|
|
203
|
+
const tokenHash = createHash("sha256").update(token, "utf8").digest("hex");
|
|
204
|
+
return db.$transaction(async (tx) => {
|
|
205
|
+
const credential = await tx.integrationCredential.findUnique({
|
|
206
|
+
where: { tokenHash },
|
|
207
|
+
include: { connection: true }
|
|
208
|
+
});
|
|
209
|
+
if (!credential?.connection.enabled || !parsePermissions(JSON.parse(credential.connection.grantedPermissions)).includes("notes:read") || !resolveManifest(credential.connection).permissions.includes("notes:read")) {
|
|
210
|
+
throw createAppError(403, "INTEGRATION_ACCESS_DENIED", "This connection cannot report activity.");
|
|
211
|
+
}
|
|
212
|
+
const report = { ...update, reportedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
213
|
+
await tx.integrationCredential.update({
|
|
214
|
+
where: { tokenHash },
|
|
215
|
+
data: { statusReport: JSON.stringify(report) }
|
|
216
|
+
});
|
|
217
|
+
return report;
|
|
218
|
+
});
|
|
146
219
|
},
|
|
147
220
|
async authenticate(token) {
|
|
148
221
|
const tokenHash = createHash("sha256").update(token, "utf8").digest("hex");
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createAppError } from "../../modules/error-handler.js";
|
|
2
|
+
import { isRecord } from "./manifest.js";
|
|
3
|
+
function parseStatusUpdate(value) {
|
|
4
|
+
if (!isRecord(value) || value.state !== "running" && value.state !== "succeeded" && value.state !== "failed" || typeof value.message !== "string" || !value.message.trim() || value.message.length > 300 || [...value.message].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) {
|
|
5
|
+
throw createAppError(
|
|
6
|
+
400,
|
|
7
|
+
"INVALID_INTEGRATION_STATUS",
|
|
8
|
+
"Provide a running, succeeded, or failed state and a message of 1\u2013300 characters without control characters."
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
return { state: value.state, message: value.message.trim() };
|
|
12
|
+
}
|
|
13
|
+
function readStatusReport(value) {
|
|
14
|
+
if (!value) return null;
|
|
15
|
+
const report = JSON.parse(value);
|
|
16
|
+
if (!isRecord(report) || typeof report.reportedAt !== "string") return null;
|
|
17
|
+
return { ...parseStatusUpdate(report), reportedAt: report.reportedAt };
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
parseStatusUpdate,
|
|
21
|
+
readStatusReport
|
|
22
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GraphQLError } from "graphql";
|
|
2
|
+
import { emitNoteChange } from "../../../features/note/services/change-events.js";
|
|
2
3
|
import { extractBlocksByType, parseNoteContent } from "../../../features/note/services/content-blocks.js";
|
|
3
4
|
import { replaceNoteReferences } from "../../../features/note/services/note-reference-index.js";
|
|
4
5
|
import {
|
|
@@ -18,7 +19,6 @@ import {
|
|
|
18
19
|
isMissingNoteVersionError,
|
|
19
20
|
isNoteVersionConflictError
|
|
20
21
|
} from "../../../features/note/services/write-conflict.js";
|
|
21
|
-
import { notifySemanticSearchNoteChanged } from "../../../features/search/note-change.js";
|
|
22
22
|
import models from "../../../models.js";
|
|
23
23
|
const PLACEHOLDER_PREFIX = "{%";
|
|
24
24
|
const PLACEHOLDER_SUFFIX = "%}";
|
|
@@ -83,7 +83,7 @@ const noteMutationResolvers = {
|
|
|
83
83
|
await replaceNoteReferences(tx, createdNote.id, replacedContent);
|
|
84
84
|
return noteResult2;
|
|
85
85
|
});
|
|
86
|
-
|
|
86
|
+
emitNoteChange({ type: "note.created", noteId: noteResult.id });
|
|
87
87
|
return noteResult;
|
|
88
88
|
},
|
|
89
89
|
updateNote: async (_, {
|
|
@@ -112,7 +112,7 @@ const noteMutationResolvers = {
|
|
|
112
112
|
if (!updatedNote) {
|
|
113
113
|
throw "NOT FOUND";
|
|
114
114
|
}
|
|
115
|
-
|
|
115
|
+
emitNoteChange({ type: "note.updated", noteId: updatedNote.id });
|
|
116
116
|
return updatedNote;
|
|
117
117
|
} catch (error) {
|
|
118
118
|
if (isNoteVersionConflictError(error)) {
|
|
@@ -146,7 +146,7 @@ const noteMutationResolvers = {
|
|
|
146
146
|
if (!trashedNote) {
|
|
147
147
|
throw "NOT FOUND";
|
|
148
148
|
}
|
|
149
|
-
|
|
149
|
+
emitNoteChange({ type: "note.deleted", noteId: Number(id) });
|
|
150
150
|
return true;
|
|
151
151
|
},
|
|
152
152
|
restoreNoteSnapshot: async (_, { id, expectedUpdatedAt }, context) => {
|
|
@@ -158,7 +158,7 @@ const noteMutationResolvers = {
|
|
|
158
158
|
if (!note) {
|
|
159
159
|
throw "NOT FOUND";
|
|
160
160
|
}
|
|
161
|
-
|
|
161
|
+
emitNoteChange({ type: "note.updated", noteId: note.id });
|
|
162
162
|
return note;
|
|
163
163
|
} catch (error) {
|
|
164
164
|
if (isNoteVersionConflictError(error)) {
|
|
@@ -185,7 +185,7 @@ const noteMutationResolvers = {
|
|
|
185
185
|
if (!note) {
|
|
186
186
|
throw "NOT FOUND";
|
|
187
187
|
}
|
|
188
|
-
|
|
188
|
+
emitNoteChange({ type: "note.created", noteId: note.id });
|
|
189
189
|
return note;
|
|
190
190
|
},
|
|
191
191
|
purgeTrashedNote: async (_, { id }) => {
|
|
@@ -193,7 +193,6 @@ const noteMutationResolvers = {
|
|
|
193
193
|
if (!purgedNote) {
|
|
194
194
|
throw "NOT FOUND";
|
|
195
195
|
}
|
|
196
|
-
notifySemanticSearchNoteChanged(Number(id));
|
|
197
196
|
return true;
|
|
198
197
|
},
|
|
199
198
|
createNotePropertyKey: async (_, { input }) => {
|
|
@@ -272,6 +271,7 @@ const noteMutationResolvers = {
|
|
|
272
271
|
if (!note) {
|
|
273
272
|
throw "NOT FOUND";
|
|
274
273
|
}
|
|
274
|
+
emitNoteChange({ type: "note.updated", noteId: note.id, affectsSearchIndex: false });
|
|
275
275
|
return note;
|
|
276
276
|
} catch (error) {
|
|
277
277
|
if (error instanceof InvalidNotePropertyInputError) {
|
|
@@ -307,10 +307,14 @@ const noteMutationResolvers = {
|
|
|
307
307
|
throw error;
|
|
308
308
|
}
|
|
309
309
|
},
|
|
310
|
-
pinNote: (_, { id, pinned }) =>
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
310
|
+
pinNote: async (_, { id, pinned }) => {
|
|
311
|
+
const note = await models.note.update({
|
|
312
|
+
where: { id: Number(id) },
|
|
313
|
+
data: { pinned: Boolean(pinned) }
|
|
314
|
+
});
|
|
315
|
+
emitNoteChange({ type: "note.updated", noteId: note.id, affectsSearchIndex: false });
|
|
316
|
+
return note;
|
|
317
|
+
},
|
|
314
318
|
reorderNotes: async (_, { notes }) => {
|
|
315
319
|
const updatePromises = notes.map(
|
|
316
320
|
({ id, order }) => models.note.update({
|
|
@@ -318,7 +322,11 @@ const noteMutationResolvers = {
|
|
|
318
322
|
data: { order }
|
|
319
323
|
})
|
|
320
324
|
);
|
|
321
|
-
|
|
325
|
+
const updatedNotes = await Promise.all(updatePromises);
|
|
326
|
+
for (const note of updatedNotes) {
|
|
327
|
+
emitNoteChange({ type: "note.updated", noteId: note.id, affectsSearchIndex: false });
|
|
328
|
+
}
|
|
329
|
+
return updatedNotes;
|
|
322
330
|
}
|
|
323
331
|
};
|
|
324
332
|
export {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { emitNoteChange } from "../../../features/note/services/change-events.js";
|
|
1
2
|
import { getNoteCleanupPreview, listNoteCleanupCandidates } from "../../../features/note/services/cleanup.js";
|
|
2
3
|
import {
|
|
3
4
|
buildNoteGraph,
|
|
@@ -293,7 +294,7 @@ const createNoteQueryResolver = (deps = {
|
|
|
293
294
|
return note;
|
|
294
295
|
}
|
|
295
296
|
try {
|
|
296
|
-
|
|
297
|
+
const updatedNote = await deps.updateNoteContent({
|
|
297
298
|
id: note.id,
|
|
298
299
|
updatedAt: note.updatedAt,
|
|
299
300
|
content: newContent,
|
|
@@ -302,6 +303,8 @@ const createNoteQueryResolver = (deps = {
|
|
|
302
303
|
content: newContent
|
|
303
304
|
})
|
|
304
305
|
});
|
|
306
|
+
emitNoteChange({ type: "note.updated", noteId: updatedNote.id });
|
|
307
|
+
return updatedNote;
|
|
305
308
|
} catch (error) {
|
|
306
309
|
if (deps.isRecordNotFoundError(error)) {
|
|
307
310
|
return note;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
const NOTE_CHANGE_CHANNEL = "note-change";
|
|
3
|
+
const noteChangeEmitter = new EventEmitter();
|
|
4
|
+
noteChangeEmitter.setMaxListeners(0);
|
|
5
|
+
const emitNoteChange = (input) => {
|
|
6
|
+
if (!Number.isInteger(input.noteId) || input.noteId <= 0) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const event = {
|
|
10
|
+
...input,
|
|
11
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12
|
+
affectsSearchIndex: input.affectsSearchIndex ?? true
|
|
13
|
+
};
|
|
14
|
+
noteChangeEmitter.emit(NOTE_CHANGE_CHANNEL, event);
|
|
15
|
+
return event;
|
|
16
|
+
};
|
|
17
|
+
const subscribeNoteChanges = (listener) => {
|
|
18
|
+
noteChangeEmitter.on(NOTE_CHANGE_CHANNEL, listener);
|
|
19
|
+
return () => {
|
|
20
|
+
noteChangeEmitter.off(NOTE_CHANGE_CHANNEL, listener);
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export {
|
|
24
|
+
emitNoteChange,
|
|
25
|
+
subscribeNoteChanges
|
|
26
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { subscribeNoteChanges } from "../../features/note/services/change-events.js";
|
|
1
2
|
import models from "../../models.js";
|
|
2
|
-
import { subscribeServerEvents } from "../../modules/server-events.js";
|
|
3
3
|
import { paths } from "../../paths.js";
|
|
4
4
|
import {
|
|
5
5
|
createEmbeddingApiKeyFingerprint,
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
createOpenAiCompatibleEmbeddingClient,
|
|
10
10
|
listOpenAiCompatibleEmbeddingModels
|
|
11
11
|
} from "./embedding-client.js";
|
|
12
|
-
import { subscribeSemanticSearchNoteChanges } from "./note-change.js";
|
|
13
12
|
import {
|
|
14
13
|
buildNoteEmbeddingChunks,
|
|
15
14
|
NOTE_EMBEDDING_TEXT_SCHEMA_VERSION
|
|
@@ -432,11 +431,9 @@ const getDefaultSemanticSearchManager = () => {
|
|
|
432
431
|
apiKeyStore: new FileEmbeddingApiKeyStore(paths.embeddingApiKey)
|
|
433
432
|
});
|
|
434
433
|
defaultSemanticSearchUnsubscribers = [
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
subscribeSemanticSearchNoteChanges((noteId) => {
|
|
439
|
-
void defaultSemanticSearchManager?.scheduleNoteSync(noteId).catch(() => void 0);
|
|
434
|
+
subscribeNoteChanges((event) => {
|
|
435
|
+
if (!event.affectsSearchIndex) return;
|
|
436
|
+
void defaultSemanticSearchManager?.scheduleNoteSync(event.noteId).catch(() => void 0);
|
|
440
437
|
})
|
|
441
438
|
];
|
|
442
439
|
defaultSemanticSearchManager.startBackgroundSync();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
+
import { emitNoteChange } from "../features/note/services/change-events.js";
|
|
2
3
|
const SERVER_EVENT_CHANNEL = "server-event";
|
|
3
4
|
const serverEventEmitter = new EventEmitter();
|
|
4
5
|
serverEventEmitter.setMaxListeners(0);
|
|
@@ -9,6 +10,8 @@ const emitServerEvent = (event) => {
|
|
|
9
10
|
...event
|
|
10
11
|
};
|
|
11
12
|
serverEventEmitter.emit(SERVER_EVENT_CHANNEL, nextEvent);
|
|
13
|
+
const type = event.type.replace(/^(mcp|integration)\./, "");
|
|
14
|
+
emitNoteChange({ type, noteId: Number(event.noteId) });
|
|
12
15
|
return nextEvent;
|
|
13
16
|
};
|
|
14
17
|
const subscribeServerEvents = (listener) => {
|