@x12i/memorix-client-api 0.0.0-stage → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/HANDOVER.md +97 -0
  2. package/dist/app.d.ts +64 -0
  3. package/dist/app.d.ts.map +1 -0
  4. package/dist/app.js +412 -0
  5. package/dist/app.js.map +1 -0
  6. package/dist/authix.d.ts +9 -0
  7. package/dist/authix.d.ts.map +1 -0
  8. package/dist/authix.js +26 -0
  9. package/dist/authix.js.map +1 -0
  10. package/dist/catalog.d.ts +160 -0
  11. package/dist/catalog.d.ts.map +1 -0
  12. package/dist/catalog.js +371 -0
  13. package/dist/catalog.js.map +1 -0
  14. package/dist/cli.d.ts +2 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +102 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/config.d.ts +33 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +109 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/derived.d.ts +2 -0
  23. package/dist/derived.d.ts.map +1 -0
  24. package/dist/derived.js +35 -0
  25. package/dist/derived.js.map +1 -0
  26. package/dist/errors.d.ts +11 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +21 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/fetch.d.ts +17 -0
  31. package/dist/fetch.d.ts.map +1 -0
  32. package/dist/fetch.js +741 -0
  33. package/dist/fetch.js.map +1 -0
  34. package/dist/index.d.ts +4 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +4 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/live-ops-nav.d.ts +2 -0
  39. package/dist/live-ops-nav.d.ts.map +1 -0
  40. package/dist/live-ops-nav.js +42 -0
  41. package/dist/live-ops-nav.js.map +1 -0
  42. package/dist/live.d.ts +20 -0
  43. package/dist/live.d.ts.map +1 -0
  44. package/dist/live.js +8 -0
  45. package/dist/live.js.map +1 -0
  46. package/dist/markdown.d.ts +19 -0
  47. package/dist/markdown.d.ts.map +1 -0
  48. package/dist/markdown.js +102 -0
  49. package/dist/markdown.js.map +1 -0
  50. package/dist/peers.d.ts +51 -0
  51. package/dist/peers.d.ts.map +1 -0
  52. package/dist/peers.js +127 -0
  53. package/dist/peers.js.map +1 -0
  54. package/dist/processed.d.ts +22 -0
  55. package/dist/processed.d.ts.map +1 -0
  56. package/dist/processed.js +397 -0
  57. package/dist/processed.js.map +1 -0
  58. package/dist/project.d.ts +25 -0
  59. package/dist/project.d.ts.map +1 -0
  60. package/dist/project.js +76 -0
  61. package/dist/project.js.map +1 -0
  62. package/dist/reads-live-routes.d.ts +4 -0
  63. package/dist/reads-live-routes.d.ts.map +1 -0
  64. package/dist/reads-live-routes.js +56 -0
  65. package/dist/reads-live-routes.js.map +1 -0
  66. package/dist/reads-live-ui.d.ts +3 -0
  67. package/dist/reads-live-ui.d.ts.map +1 -0
  68. package/dist/reads-live-ui.js +204 -0
  69. package/dist/reads-live-ui.js.map +1 -0
  70. package/dist/reads-ring.d.ts +29 -0
  71. package/dist/reads-ring.d.ts.map +1 -0
  72. package/dist/reads-ring.js +44 -0
  73. package/dist/reads-ring.js.map +1 -0
  74. package/dist/refs.d.ts +24 -0
  75. package/dist/refs.d.ts.map +1 -0
  76. package/dist/refs.js +354 -0
  77. package/dist/refs.js.map +1 -0
  78. package/dist/request-countex.d.ts +53 -0
  79. package/dist/request-countex.d.ts.map +1 -0
  80. package/dist/request-countex.js +171 -0
  81. package/dist/request-countex.js.map +1 -0
  82. package/dist/scope.d.ts +13 -0
  83. package/dist/scope.d.ts.map +1 -0
  84. package/dist/scope.js +46 -0
  85. package/dist/scope.js.map +1 -0
  86. package/dist/scoper.d.ts +75 -0
  87. package/dist/scoper.d.ts.map +1 -0
  88. package/dist/scoper.js +475 -0
  89. package/dist/scoper.js.map +1 -0
  90. package/dist/shaped-refs.d.ts +23 -0
  91. package/dist/shaped-refs.d.ts.map +1 -0
  92. package/dist/shaped-refs.js +395 -0
  93. package/dist/shaped-refs.js.map +1 -0
  94. package/dist/skills.d.ts +102 -0
  95. package/dist/skills.d.ts.map +1 -0
  96. package/dist/skills.js +609 -0
  97. package/dist/skills.js.map +1 -0
  98. package/dist/story.d.ts +43 -0
  99. package/dist/story.d.ts.map +1 -0
  100. package/dist/story.js +121 -0
  101. package/dist/story.js.map +1 -0
  102. package/dist/upstream.d.ts +13 -0
  103. package/dist/upstream.d.ts.map +1 -0
  104. package/dist/upstream.js +58 -0
  105. package/dist/upstream.js.map +1 -0
  106. package/openapi/openapi.json +480 -0
  107. package/package.json +67 -6
  108. package/README.md +0 -3
package/dist/fetch.js ADDED
@@ -0,0 +1,741 @@
1
+ import { findContentType, findObjectType } from "./catalog.js";
2
+ import { chooseStory, declaredTemplate, parseStoryVariant, STORY_AI_TYPE, STORY_TYPE, storyTemplates, } from "./story.js";
3
+ import { ApiError, UpstreamError } from "./errors.js";
4
+ import { scopeBody } from "./scope.js";
5
+ import { SCOPER_MCP_TOOL_NAMES, SCOPER_RELEASE } from "./scoper.js";
6
+ import { asRecord } from "./upstream.js";
7
+ /** Read-only Nexum tools served by memorix-service `POST /mcp/nexum`. */
8
+ export const NEXUM_MCP_TOOLS = [
9
+ "nexum_list_snapshots",
10
+ "nexum_get_snapshot",
11
+ "nexum_get_connected_content",
12
+ "nexum_knowledge_status",
13
+ "nexum_list_entities",
14
+ "nexum_get_entity",
15
+ "nexum_query",
16
+ ];
17
+ /**
18
+ * Memorix POST routes whose body is a read (compose, list, item, canvas query).
19
+ * Writes such as `/api/data/content` are not in this map.
20
+ */
21
+ const READ_POSTS = {
22
+ "/api/v1/data/compose": "/api/data/compose",
23
+ "/api/v1/data/compose/data": "/api/data/compose/data",
24
+ "/api/v1/data/compose/data/list": "/api/data/compose/data/list",
25
+ "/api/v1/data/lists": "/api/data/lists",
26
+ "/api/v1/data/items": "/api/data/items",
27
+ "/api/v1/data-journey/matrix/query": "/api/data-journey/matrix/query",
28
+ "/api/v1/data-journey/matrix/columns/query": "/api/data-journey/matrix/columns/query",
29
+ "/api/v1/data-journey/matrix/cells/query": "/api/data-journey/matrix/cells/query",
30
+ "/api/v1/intelligence/anomalies/content-types": "/api/intelligence/anomalies/content-types",
31
+ };
32
+ /** Story drawings are content rows. Several rows may share this value; the newest revision is the drawing. */
33
+ const TEMPLATE_PATH = "data.template";
34
+ const MAX_SOURCE_REFS = 16;
35
+ const MAX_CONTENT_TYPES = 16;
36
+ const INSTANCE_PAGES = 5;
37
+ /** Messages Memorix relays when it cannot open a connection to the Nexum explore REST. */
38
+ const UNREACHABLE = /fetch failed|ECONNREFUSED|ECONNRESET|ENOTFOUND|EHOSTUNREACH|ETIMEDOUT|timed? ?out|aborted/i;
39
+ export function registerFetchRoutes(app, options) {
40
+ const memorixBase = options.memorixUrl.replace(/\/+$/, "");
41
+ const nexumMcpEndpoint = `${memorixBase}/mcp/nexum`;
42
+ let discoveredMgc = options.mgcMcpUrl === undefined ? undefined : normalizeMcpUrl(options.mgcMcpUrl);
43
+ app.get("/api/v1/memorix/*", async (req) => {
44
+ const scope = await options.scopeFor(req);
45
+ const upstreamPath = memorixPath(wildcard(req), searchOf(req));
46
+ assertFetchAllowed(upstreamPath);
47
+ return options.upstream.get(upstreamPath, scope);
48
+ });
49
+ app.get("/api/v1/memorix/data/content", async (req) => {
50
+ const scope = await options.scopeFor(req);
51
+ const query = (req.query ?? {});
52
+ const objectType = queryValue(query.objectType);
53
+ const contentType = queryValue(query.contentType);
54
+ const recordId = queryValue(query.recordId) ?? queryValue(query.id);
55
+ const explicit = queryValue(query.discriminatorValue);
56
+ const rawFilter = queryValue(query.filter);
57
+ const counting = queryValue(query.count) === "true";
58
+ const passThrough = () => options.upstream.get(`/api/data/content${searchOf(req)}`, scope);
59
+ // Lists, counts, and reads with no selector are answered by Memorix.
60
+ // Service 3.16 reads `discriminatorValue` on a list and rejects a query parameter it does not know.
61
+ if (!objectType || !contentType || counting || !recordId)
62
+ return passThrough();
63
+ if (!explicit && !rawFilter)
64
+ return passThrough();
65
+ const filters = parseFilter(rawFilter);
66
+ const path = await discriminatorPath(scope, objectType, contentType);
67
+ const value = path ? (explicit ?? discriminatorFromFilter(filters, path)) : undefined;
68
+ const templateValue = discriminatorFromFilter(filters, TEMPLATE_PATH);
69
+ const selector = path && value !== undefined
70
+ ? { path, value }
71
+ : templateValue !== undefined
72
+ ? { path: TEMPLATE_PATH, value: templateValue }
73
+ : null;
74
+ if (!selector)
75
+ return passThrough();
76
+ const rows = await recordInstances(scope, objectType, contentType, recordId, queryValue(query.include), queryValue(query.includeDeleted));
77
+ return contentInstance(scope, objectType, contentType, recordId, selector.path, selector.value, rows);
78
+ });
79
+ app.get("/api/v1/data/story", async (req, reply) => {
80
+ const scope = await options.scopeFor(req);
81
+ const query = (req.query ?? {});
82
+ const objectType = queryValue(query.objectType);
83
+ const recordId = queryValue(query.recordId) ?? queryValue(query.id);
84
+ const template = queryValue(query.template);
85
+ if (!objectType || !recordId || !template) {
86
+ throw new ApiError(400, "VALIDATION", "objectType, recordId, and template are required");
87
+ }
88
+ const variant = parseStoryVariant(queryValue(query.variant));
89
+ const view = findObjectType(await options.loadCatalog(options.upstream, scope), objectType);
90
+ if (!view)
91
+ return options.upstream.get(storyContentPath(objectType, STORY_TYPE, recordId, template), scope);
92
+ const storyType = findContentType(view, STORY_TYPE);
93
+ const aiType = findContentType(view, STORY_AI_TYPE);
94
+ if (!declaredTemplate(storyType, template)) {
95
+ if (!storyType)
96
+ return options.upstream.get(storyContentPath(objectType, STORY_TYPE, recordId, template), scope);
97
+ const rows = await recordInstances(scope, objectType, STORY_TYPE, recordId, undefined, undefined);
98
+ return contentInstance(scope, objectType, STORY_TYPE, recordId, TEMPLATE_PATH, template, rows);
99
+ }
100
+ const [storyRows, aiRows] = await Promise.all([
101
+ storyType ? recordInstances(scope, objectType, STORY_TYPE, recordId, undefined, undefined) : Promise.resolve([]),
102
+ aiType ? recordInstances(scope, objectType, STORY_AI_TYPE, recordId, undefined, undefined) : Promise.resolve([]),
103
+ ]);
104
+ const choice = chooseStory({
105
+ variant,
106
+ template,
107
+ storyDeclared: Boolean(storyType),
108
+ aiDeclared: Boolean(aiType),
109
+ storyRows,
110
+ aiRows,
111
+ });
112
+ if (!choice.record || !choice.contentType) {
113
+ return reply.status(404).send({
114
+ error: { code: "STORY_NOT_FOUND", message: `No usable ${template} story for ${recordId}` },
115
+ story: choice.block,
116
+ });
117
+ }
118
+ const revision = typeof choice.record.revision === "number" ? choice.record.revision : null;
119
+ return {
120
+ scope: scopeBody(scope),
121
+ addressing: {
122
+ orgId: scope.orgId,
123
+ objectType,
124
+ contentType: choice.contentType,
125
+ id: recordId,
126
+ recordId,
127
+ discriminator: { path: TEMPLATE_PATH, value: template },
128
+ },
129
+ record: choice.record,
130
+ currentRevision: revision,
131
+ unresolvedRelationships: [],
132
+ story: choice.block,
133
+ };
134
+ });
135
+ app.get("/api/v1/data/stories", async (req) => {
136
+ const scope = await options.scopeFor(req);
137
+ const query = (req.query ?? {});
138
+ const objectType = queryValue(query.objectType);
139
+ const recordId = queryValue(query.recordId) ?? queryValue(query.id);
140
+ if (!objectType || !recordId) {
141
+ throw new ApiError(400, "VALIDATION", "objectType and recordId are required");
142
+ }
143
+ const variant = parseStoryVariant(queryValue(query.variant));
144
+ const view = findObjectType(await options.loadCatalog(options.upstream, scope), objectType);
145
+ if (!view)
146
+ return options.upstream.get(storyContentPath(objectType, STORY_TYPE, recordId), scope);
147
+ const storyType = findContentType(view, STORY_TYPE);
148
+ const aiType = findContentType(view, STORY_AI_TYPE);
149
+ const templates = storyTemplates(storyType);
150
+ const [storyRows, aiRows] = await Promise.all([
151
+ storyType ? recordInstances(scope, objectType, STORY_TYPE, recordId, undefined, undefined) : Promise.resolve([]),
152
+ aiType ? recordInstances(scope, objectType, STORY_AI_TYPE, recordId, undefined, undefined) : Promise.resolve([]),
153
+ ]);
154
+ return {
155
+ scope: scopeBody(scope),
156
+ objectType,
157
+ recordId,
158
+ stories: templates.map((item) => {
159
+ const choice = chooseStory({
160
+ variant,
161
+ template: item.value,
162
+ storyDeclared: Boolean(storyType),
163
+ aiDeclared: Boolean(aiType),
164
+ storyRows,
165
+ aiRows,
166
+ });
167
+ return {
168
+ template: item.value,
169
+ ...(item.label ? { label: item.label } : {}),
170
+ story: choice.block,
171
+ record: choice.record,
172
+ };
173
+ }),
174
+ };
175
+ });
176
+ for (const [publicPath, upstreamPath] of Object.entries(READ_POSTS)) {
177
+ app.post(publicPath, async (req) => {
178
+ const scope = await options.scopeFor(req);
179
+ return options.upstream.post(`${upstreamPath}${searchOf(req)}`, scope, req.body ?? {});
180
+ });
181
+ }
182
+ app.post("/api/v1/nexum/query", async (req) => {
183
+ const scope = await options.scopeFor(req);
184
+ return nexumCanvas(() => options.upstream.post(`/api/knowledge/query${searchOf(req)}`, scope, req.body ?? {}));
185
+ });
186
+ async function discriminatorPath(scope, objectType, contentType) {
187
+ const view = findObjectType(await options.loadCatalog(options.upstream, scope), objectType);
188
+ const contentTypeView = view ? findContentType(view, contentType) : null;
189
+ return contentTypeView?.discriminator?.path ?? null;
190
+ }
191
+ async function recordInstances(scope, objectType, contentType, recordId, include, includeDeleted) {
192
+ const rows = [];
193
+ const seen = new Set();
194
+ let cursor;
195
+ for (let page = 0; page < INSTANCE_PAGES; page += 1) {
196
+ const params = new URLSearchParams({
197
+ objectType,
198
+ contentType,
199
+ limit: "200",
200
+ filter: JSON.stringify([{ field: "recordId", op: "in", value: [recordId] }]),
201
+ });
202
+ if (include)
203
+ params.set("include", include);
204
+ if (includeDeleted)
205
+ params.set("includeDeleted", includeDeleted);
206
+ if (cursor)
207
+ params.set("cursor", cursor);
208
+ const body = asRecord(await options.upstream.get(`/api/data/content?${params.toString()}`, scope));
209
+ for (const item of Array.isArray(body?.records) ? body.records : []) {
210
+ const row = asRecord(item);
211
+ if (row && (row.recordId === undefined || row.recordId === recordId))
212
+ rows.push(row);
213
+ }
214
+ const next = asRecord(body?.pageInfo)?.nextCursor;
215
+ if (typeof next !== "string" || !next || seen.has(next))
216
+ break;
217
+ seen.add(next);
218
+ cursor = next;
219
+ }
220
+ return rows;
221
+ }
222
+ app.get("/api/v1/nexum/status", async (req) => {
223
+ const scope = await options.scopeFor(req);
224
+ const body = asRecord(await options.upstream.get("/api/knowledge/status", scope));
225
+ if (!body)
226
+ return { scope: scopeBody(scope) };
227
+ return {
228
+ ...body,
229
+ upstreamMcpUrl: body.mcpUrl,
230
+ mcpUrl: publicUrl(req, "/api/v1/mcp/nexum"),
231
+ };
232
+ });
233
+ app.get("/api/v1/nexum/entities", async (req) => {
234
+ const scope = await options.scopeFor(req);
235
+ return nexumCanvas(() => options.upstream.get(`/api/knowledge/entities${searchOf(req)}`, scope));
236
+ });
237
+ app.get("/api/v1/nexum/entities/*", async (req) => {
238
+ const scope = await options.scopeFor(req);
239
+ const entityId = wildcard(req);
240
+ if (!entityId)
241
+ throw new ApiError(400, "VALIDATION", "entityId required");
242
+ return nexumCanvas(() => options.upstream.get(`/api/knowledge/entities/${encodeURIComponent(entityId)}${searchOf(req)}`, scope));
243
+ });
244
+ app.get("/api/v1/nexum/snapshots", async (req) => {
245
+ const scope = await options.scopeFor(req);
246
+ const query = (req.query ?? {});
247
+ const limit = limitOf(query.limit);
248
+ const cursor = typeof query.cursor === "string" && query.cursor ? query.cursor : undefined;
249
+ const params = new URLSearchParams({
250
+ objectType: "content",
251
+ contentType: "snapshots",
252
+ limit: String(limit),
253
+ });
254
+ if (cursor)
255
+ params.set("cursor", cursor);
256
+ const listed = asRecord(await options.upstream.get(`/api/data/content?${params.toString()}`, scope));
257
+ const records = Array.isArray(listed?.records) ? listed.records : [];
258
+ const kept = truthy(query.knowledgeGraphOnly) ? records.filter(isKnowledgeGraph) : records;
259
+ const pageInfo = asRecord(listed?.pageInfo);
260
+ return {
261
+ ...(listed ?? {}),
262
+ scope: listed?.scope ?? scopeBody(scope),
263
+ addressing: listed?.addressing ?? { objectType: "content", contentType: "snapshots" },
264
+ records: kept,
265
+ pageInfo: {
266
+ nextCursor: typeof pageInfo?.nextCursor === "string" ? pageInfo.nextCursor : null,
267
+ limit: typeof pageInfo?.limit === "number" ? pageInfo.limit : limit,
268
+ },
269
+ };
270
+ });
271
+ app.get("/api/v1/nexum/snapshots/*", async (req) => {
272
+ const scope = await options.scopeFor(req);
273
+ const raw = wildcard(req);
274
+ const connected = raw.endsWith("/connected");
275
+ const recordId = (connected ? raw.slice(0, -"/connected".length) : raw).replace(/\/$/, "");
276
+ if (!recordId)
277
+ throw new ApiError(400, "VALIDATION", "recordId required");
278
+ const params = new URLSearchParams({
279
+ objectType: "content",
280
+ contentType: "snapshots",
281
+ recordId,
282
+ });
283
+ const loaded = await options.upstream.get(`/api/data/content?${params.toString()}`, scope);
284
+ const record = contentRecord(loaded);
285
+ if (!connected) {
286
+ const body = asRecord(loaded) ?? {};
287
+ return { ...body, knowledgeGraph: isKnowledgeGraph(record) };
288
+ }
289
+ const sourceRefs = sourceRefsOf(record).slice(0, MAX_SOURCE_REFS);
290
+ const contentTypes = contentTypesOf(req.query?.contentTypes);
291
+ const rows = [];
292
+ for (const sourceRecordId of sourceRefs) {
293
+ rows.push(await connectedContent(options.upstream, scope, sourceRecordId, contentTypes));
294
+ }
295
+ return {
296
+ scope: scopeBody(scope),
297
+ snapshot: {
298
+ objectType: "content",
299
+ contentType: "snapshots",
300
+ recordId,
301
+ knowledgeGraph: isKnowledgeGraph(record),
302
+ },
303
+ sourceRefs,
304
+ connected: rows,
305
+ };
306
+ });
307
+ app.get("/api/v1/mcp", async (req) => {
308
+ const scope = await options.scopeFor(req);
309
+ const mgcUrl = await mgcEndpoint();
310
+ let nexum = null;
311
+ let nexumError;
312
+ try {
313
+ const advertised = await exchange(options.fetchImpl, "GET", nexumMcpEndpoint, scope);
314
+ nexum = advertised.body;
315
+ }
316
+ catch (err) {
317
+ nexumError = err instanceof Error ? err.message : String(err);
318
+ }
319
+ return {
320
+ scope: scopeBody(scope),
321
+ servers: [
322
+ {
323
+ id: "memorix-nexum",
324
+ protocol: "mcp",
325
+ transport: "json-rpc",
326
+ url: publicUrl(req, "/api/v1/mcp/nexum"),
327
+ upstream: nexumMcpEndpoint,
328
+ tools: NEXUM_MCP_TOOLS,
329
+ advertise: nexum,
330
+ ...(nexumError ? { error: nexumError } : {}),
331
+ },
332
+ {
333
+ id: "memorix-scoper",
334
+ protocol: "mcp",
335
+ transport: "json-rpc",
336
+ url: publicUrl(req, "/api/v1/mcp/scoper"),
337
+ package: `${SCOPER_RELEASE.package}@${SCOPER_RELEASE.version}`,
338
+ tools: SCOPER_MCP_TOOL_NAMES,
339
+ note: "Scope and subject reads hosted in this process. Saved functions are used when SCOPER_FUNCTIONS_DIR is set.",
340
+ },
341
+ {
342
+ id: "memorix-mgc",
343
+ protocol: "mcp",
344
+ transport: "json-rpc",
345
+ url: mgcUrl ? publicUrl(req, "/api/v1/mcp/mgc") : null,
346
+ upstream: mgcUrl,
347
+ configured: Boolean(mgcUrl),
348
+ tools: ["mgc_list_connectors", "mgc_describe_connector", "mgc_fetch"],
349
+ note: "Connector catalog and raw fetch on the MGC source host. Set MGC_MCP_URL or MGC_SOURCE_SERVICE_URL, or rely on Memorix GET /api/connectors genericConnectors.mcpUrl.",
350
+ },
351
+ ],
352
+ };
353
+ async function mgcEndpoint() {
354
+ if (discoveredMgc !== undefined)
355
+ return discoveredMgc;
356
+ try {
357
+ const body = asRecord(await options.upstream.get("/api/connectors", scope));
358
+ const generic = asRecord(body?.genericConnectors);
359
+ const url = typeof generic?.mcpUrl === "string" ? generic.mcpUrl.trim() : "";
360
+ discoveredMgc = url ? url.replace(/\/+$/, "") : null;
361
+ }
362
+ catch {
363
+ return null;
364
+ }
365
+ return discoveredMgc;
366
+ }
367
+ });
368
+ app.route({
369
+ method: ["GET", "POST"],
370
+ url: "/api/v1/mcp/nexum",
371
+ handler: async (req, reply) => {
372
+ const scope = await options.scopeFor(req);
373
+ await proxyMcp(options.fetchImpl, req, reply, nexumMcpEndpoint, scope, "memorix-nexum");
374
+ },
375
+ });
376
+ app.route({
377
+ method: ["GET", "POST"],
378
+ url: "/api/v1/mcp/mgc",
379
+ handler: async (req, reply) => {
380
+ const scope = await options.scopeFor(req);
381
+ const target = await resolveMgc(scope);
382
+ if (!target) {
383
+ throw new ApiError(404, "MGC_NOT_CONFIGURED", "MGC MCP URL is not configured. Set MGC_MCP_URL or MGC_SOURCE_SERVICE_URL, or expose genericConnectors.mcpUrl from Memorix.");
384
+ }
385
+ await proxyMcp(options.fetchImpl, req, reply, target, scope, "memorix-mgc");
386
+ },
387
+ });
388
+ async function resolveMgc(scope) {
389
+ if (discoveredMgc !== undefined)
390
+ return discoveredMgc;
391
+ try {
392
+ const body = asRecord(await options.upstream.get("/api/connectors", scope));
393
+ const generic = asRecord(body?.genericConnectors);
394
+ const url = typeof generic?.mcpUrl === "string" ? generic.mcpUrl.trim() : "";
395
+ discoveredMgc = url ? url.replace(/\/+$/, "") : null;
396
+ }
397
+ catch {
398
+ return null;
399
+ }
400
+ return discoveredMgc;
401
+ }
402
+ }
403
+ async function nexumCanvas(call) {
404
+ try {
405
+ return await call();
406
+ }
407
+ catch (err) {
408
+ if (err instanceof UpstreamError && err.statusCode === 500 && errorCode(err.body) === "INTERNAL_ERROR") {
409
+ const message = upstreamMessage(err.body);
410
+ if (UNREACHABLE.test(message)) {
411
+ throw new ApiError(502, "NEXUM_UPSTREAM_UNREACHABLE", `Nexum explore service is unreachable from the Memorix host (Memorix said: ${message}). See GET /api/v1/nexum/status.`);
412
+ }
413
+ }
414
+ throw err;
415
+ }
416
+ }
417
+ function queryValue(raw) {
418
+ const value = Array.isArray(raw) ? raw[0] : raw;
419
+ if (typeof value !== "string")
420
+ return undefined;
421
+ const trimmed = value.trim();
422
+ return trimmed || undefined;
423
+ }
424
+ /** SafeFilter array, one SafeFilter, or an equality map. `null` when the text is not one of those. */
425
+ function parseFilter(raw) {
426
+ if (!raw)
427
+ return null;
428
+ let parsed;
429
+ try {
430
+ parsed = JSON.parse(raw);
431
+ }
432
+ catch {
433
+ return null;
434
+ }
435
+ const clause = (item) => {
436
+ const record = asRecord(item);
437
+ return record && typeof record.field === "string" && typeof record.op === "string"
438
+ ? { field: record.field, op: record.op, value: record.value }
439
+ : null;
440
+ };
441
+ if (Array.isArray(parsed)) {
442
+ const clauses = parsed.map(clause);
443
+ return clauses.every((item) => item !== null) ? clauses : null;
444
+ }
445
+ const single = clause(parsed);
446
+ if (single)
447
+ return [single];
448
+ const map = asRecord(parsed);
449
+ if (!map)
450
+ return null;
451
+ return Object.entries(map).map(([field, value]) => ({ field, op: "eq", value }));
452
+ }
453
+ function discriminatorFromFilter(filters, path) {
454
+ for (const filter of filters ?? []) {
455
+ if (filter.field !== path || filter.op !== "eq")
456
+ continue;
457
+ const value = filter.value;
458
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
459
+ return String(value);
460
+ }
461
+ }
462
+ return undefined;
463
+ }
464
+ function stringAt(root, path) {
465
+ let current = root;
466
+ for (const part of path.split(".").filter(Boolean)) {
467
+ const record = asRecord(current);
468
+ if (!record)
469
+ return undefined;
470
+ current = record[part];
471
+ }
472
+ if (typeof current === "string" || typeof current === "number" || typeof current === "boolean") {
473
+ return String(current);
474
+ }
475
+ return undefined;
476
+ }
477
+ function storyContentPath(objectType, contentType, recordId, template) {
478
+ const params = new URLSearchParams({ objectType, contentType, recordId });
479
+ if (template) {
480
+ params.set("filter", JSON.stringify([{ field: TEMPLATE_PATH, op: "eq", value: template }]));
481
+ }
482
+ return `/api/data/content?${params.toString()}`;
483
+ }
484
+ function contentInstance(scope, objectType, contentType, recordId, path, value, rows) {
485
+ const record = latest(rows.filter((row) => stringAt(row, path) === value));
486
+ if (!record) {
487
+ throw new ApiError(404, "READ_NOT_FOUND", notFoundMessage(rows, recordId, contentType, path, value));
488
+ }
489
+ return {
490
+ scope: scopeBody(scope),
491
+ addressing: {
492
+ orgId: scope.orgId,
493
+ objectType,
494
+ contentType,
495
+ id: recordId,
496
+ recordId,
497
+ discriminator: { path, value },
498
+ },
499
+ record,
500
+ currentRevision: typeof record.revision === "number" ? record.revision : null,
501
+ unresolvedRelationships: [],
502
+ };
503
+ }
504
+ function latest(rows) {
505
+ let best;
506
+ for (const row of rows) {
507
+ const revision = typeof row.revision === "number" ? row.revision : -1;
508
+ const bestRevision = typeof best?.revision === "number" ? best.revision : -1;
509
+ if (!best || revision > bestRevision)
510
+ best = row;
511
+ }
512
+ return best;
513
+ }
514
+ function notFoundMessage(rows, recordId, contentType, path, value) {
515
+ if (rows.length === 0)
516
+ return `Content not found for ${recordId}/${contentType}`;
517
+ const stored = [...new Set(rows.map((row) => stringAt(row, path)).filter((v) => v !== undefined))];
518
+ const present = stored.length > 0
519
+ ? `Stored values: ${stored.map((v) => JSON.stringify(v)).join(", ")}.`
520
+ : `The stored ${rows.length === 1 ? "row has" : "rows have"} no ${path}.`;
521
+ return `No ${contentType} instance of ${recordId} has ${path} = ${JSON.stringify(value)}. ${present}`;
522
+ }
523
+ function upstreamMessage(body) {
524
+ const error = asRecord(asRecord(body)?.error);
525
+ return typeof error?.message === "string" ? error.message : "";
526
+ }
527
+ function normalizeMcpUrl(url) {
528
+ const trimmed = url?.trim() ?? "";
529
+ return trimmed ? trimmed.replace(/\/+$/, "") : null;
530
+ }
531
+ function wildcard(req) {
532
+ const params = req.params;
533
+ return (params["*"] ?? params.path ?? "").trim();
534
+ }
535
+ function searchOf(req) {
536
+ const index = req.url.indexOf("?");
537
+ return index >= 0 ? req.url.slice(index) : "";
538
+ }
539
+ function memorixPath(raw, search) {
540
+ if (!raw || raw.includes("..") || raw.includes("\\")) {
541
+ throw new ApiError(400, "VALIDATION", "memorix path required");
542
+ }
543
+ const encoded = raw
544
+ .split("/")
545
+ .filter((part) => part.length > 0)
546
+ .map((part) => encodeURIComponent(part))
547
+ .join("/");
548
+ if (!encoded)
549
+ throw new ApiError(400, "VALIDATION", "memorix path required");
550
+ return `/api/${encoded}${search}`;
551
+ }
552
+ function assertFetchAllowed(upstreamPath) {
553
+ const pathname = upstreamPath.split("?")[0] ?? upstreamPath;
554
+ if (pathname === "/api/relationships/links" || pathname.startsWith("/api/relationships/links/")) {
555
+ throw new ApiError(403, "FETCH_DENIED", "Relationship links are not a fetch route");
556
+ }
557
+ if (/^\/api\/restricted-store\/(?:.*\/)?content$/.test(pathname)) {
558
+ throw new ApiError(403, "FETCH_DENIED", "Break-glass content stays on the Memorix host");
559
+ }
560
+ }
561
+ function publicUrl(req, path) {
562
+ const forwardedProto = headerValue(req.headers["x-forwarded-proto"]);
563
+ const proto = forwardedProto?.split(",")[0]?.trim() || req.protocol || "http";
564
+ const host = headerValue(req.headers["x-forwarded-host"]) ?? headerValue(req.headers.host) ?? "127.0.0.1:5120";
565
+ return `${proto}://${host}${path}`;
566
+ }
567
+ function headerValue(value) {
568
+ const raw = Array.isArray(value) ? value[0] : value;
569
+ const trimmed = raw?.trim();
570
+ return trimmed || undefined;
571
+ }
572
+ function limitOf(raw) {
573
+ if (raw === undefined || raw === "")
574
+ return 50;
575
+ const limit = Number(raw);
576
+ if (!Number.isFinite(limit))
577
+ throw new ApiError(400, "VALIDATION", "limit must be a number");
578
+ return Math.min(Math.max(Math.trunc(limit), 1), 200);
579
+ }
580
+ function truthy(raw) {
581
+ return raw === true || raw === "true" || raw === "1";
582
+ }
583
+ function isKnowledgeGraph(record) {
584
+ const data = asRecord(asRecord(record)?.data);
585
+ return data?.kind === "knowledge-graph";
586
+ }
587
+ function contentRecord(body) {
588
+ const root = asRecord(body);
589
+ if (!root)
590
+ return body;
591
+ return root.record ?? root;
592
+ }
593
+ function sourceRefsOf(record) {
594
+ const data = asRecord(asRecord(record)?.data);
595
+ const refs = data?.sourceRefs;
596
+ if (!Array.isArray(refs))
597
+ return [];
598
+ const out = [];
599
+ for (const ref of refs) {
600
+ const id = String(ref ?? "").trim();
601
+ if (id && !out.includes(id))
602
+ out.push(id);
603
+ }
604
+ return out;
605
+ }
606
+ function contentTypesOf(raw) {
607
+ if (raw === undefined || raw === null || raw === "")
608
+ return undefined;
609
+ let items;
610
+ if (Array.isArray(raw))
611
+ items = raw;
612
+ else if (typeof raw === "string") {
613
+ const trimmed = raw.trim();
614
+ if (!trimmed)
615
+ return undefined;
616
+ if (trimmed.startsWith("[")) {
617
+ try {
618
+ const parsed = JSON.parse(trimmed);
619
+ if (!Array.isArray(parsed))
620
+ throw new ApiError(400, "VALIDATION", "contentTypes must be a list of strings");
621
+ items = parsed;
622
+ }
623
+ catch (err) {
624
+ if (err instanceof ApiError)
625
+ throw err;
626
+ throw new ApiError(400, "VALIDATION", "contentTypes must be JSON or comma-separated");
627
+ }
628
+ }
629
+ else {
630
+ items = trimmed.split(",");
631
+ }
632
+ }
633
+ else {
634
+ throw new ApiError(400, "VALIDATION", "contentTypes must be a list of strings");
635
+ }
636
+ const out = [];
637
+ for (const item of items) {
638
+ const id = String(item ?? "").trim();
639
+ if (!id || out.includes(id))
640
+ continue;
641
+ out.push(id);
642
+ if (out.length > MAX_CONTENT_TYPES) {
643
+ throw new ApiError(400, "VALIDATION", `contentTypes exceeds ${MAX_CONTENT_TYPES}`);
644
+ }
645
+ }
646
+ return out.length > 0 ? out : undefined;
647
+ }
648
+ async function connectedContent(upstream, scope, recordId, contentTypes) {
649
+ try {
650
+ const composed = asRecord(await upstream.post("/api/data/compose/data", scope, {
651
+ recordId,
652
+ ...(contentTypes ? { contentTypes } : {}),
653
+ }));
654
+ return {
655
+ recordId,
656
+ found: true,
657
+ objectType: typeof composed?.objectType === "string" ? composed.objectType : null,
658
+ dataCategory: typeof composed?.dataCategory === "string" ? composed.dataCategory : null,
659
+ contents: composed?.contents ?? {},
660
+ warnings: composed?.warnings ?? [],
661
+ };
662
+ }
663
+ catch (err) {
664
+ if (!(err instanceof UpstreamError))
665
+ throw err;
666
+ const code = errorCode(err.body);
667
+ if (err.statusCode === 404 || code === "READ_NOT_FOUND" || code === "READ_UNAVAILABLE") {
668
+ return {
669
+ recordId,
670
+ found: false,
671
+ code: code || "READ_NOT_FOUND",
672
+ message: errorMessage(err.body),
673
+ };
674
+ }
675
+ throw err;
676
+ }
677
+ }
678
+ function errorCode(body) {
679
+ const error = asRecord(asRecord(body)?.error);
680
+ return typeof error?.code === "string" ? error.code : "";
681
+ }
682
+ function errorMessage(body) {
683
+ const error = asRecord(asRecord(body)?.error);
684
+ if (typeof error?.message === "string")
685
+ return error.message;
686
+ return "Subject was not found";
687
+ }
688
+ function scopeHeaders(scope) {
689
+ return {
690
+ accept: "application/json",
691
+ "content-type": "application/json",
692
+ "x-memorix-org-id": scope.orgId,
693
+ "x-memorix-agent-ids": scope.agentIds.join(","),
694
+ "x-memorix-caller": scope.caller,
695
+ };
696
+ }
697
+ async function exchange(fetchImpl, method, url, scope, body) {
698
+ let res;
699
+ try {
700
+ res = await fetchImpl(url, {
701
+ method,
702
+ headers: scopeHeaders(scope),
703
+ body: body === undefined ? undefined : JSON.stringify(body),
704
+ });
705
+ }
706
+ catch (err) {
707
+ const message = err instanceof Error ? err.message : String(err);
708
+ throw new UpstreamError(502, { error: { code: "UPSTREAM", message } });
709
+ }
710
+ const text = await res.text();
711
+ if (!text)
712
+ return { status: res.status, body: null };
713
+ try {
714
+ return { status: res.status, body: JSON.parse(text) };
715
+ }
716
+ catch {
717
+ return { status: res.status, body: text };
718
+ }
719
+ }
720
+ async function proxyMcp(fetchImpl, req, reply, target, scope, server) {
721
+ const method = req.method === "POST" ? "POST" : "GET";
722
+ const exchanged = await exchange(fetchImpl, method, target, scope, method === "POST" ? (req.body ?? {}) : undefined);
723
+ let payload = exchanged.body;
724
+ if (method === "GET" && server === "memorix-nexum") {
725
+ const body = asRecord(payload);
726
+ if (body) {
727
+ payload = {
728
+ ...body,
729
+ upstreamMcpUrl: body.mcpUrl,
730
+ mcpUrl: publicUrl(req, "/api/v1/mcp/nexum"),
731
+ };
732
+ }
733
+ }
734
+ reply.code(exchanged.status);
735
+ if (payload === null || payload === undefined) {
736
+ await reply.send();
737
+ return;
738
+ }
739
+ await reply.send(payload);
740
+ }
741
+ //# sourceMappingURL=fetch.js.map