@remnic/core 9.3.767 → 9.3.769
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/access-cli.js +5 -5
- package/dist/access-http.d.ts +51 -1
- package/dist/access-http.js +2 -2
- package/dist/access-mcp.d.ts +7 -1
- package/dist/access-mcp.js +5 -3
- package/dist/{chunk-TVLN5EZZ.js → chunk-CNXMWYLA.js} +78 -3
- package/dist/chunk-CNXMWYLA.js.map +1 -0
- package/dist/{chunk-UG274TNV.js → chunk-E2SPGGUI.js} +13 -8
- package/dist/chunk-E2SPGGUI.js.map +1 -0
- package/dist/{chunk-HBOPSFQQ.js → chunk-EVXI2I6G.js} +3 -3
- package/dist/{chunk-4DLFJJOQ.js → chunk-QMN3CIFS.js} +2 -2
- package/dist/{chunk-J3UJJZKI.js → chunk-SIPZ5UMK.js} +85 -5
- package/dist/chunk-SIPZ5UMK.js.map +1 -0
- package/dist/cli.js +3 -3
- package/dist/connectors/index.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +11 -5
- package/dist/orchestrator.js +5 -5
- package/dist/tokens.d.ts +3 -1
- package/dist/tokens.js +3 -1
- package/package.json +2 -2
- package/src/access-http.test.ts +350 -0
- package/src/access-http.ts +160 -6
- package/src/access-mcp.ts +120 -3
- package/src/index.ts +3 -0
- package/src/tokens.test.ts +58 -0
- package/src/tokens.ts +19 -8
- package/dist/chunk-J3UJJZKI.js.map +0 -1
- package/dist/chunk-TVLN5EZZ.js.map +0 -1
- package/dist/chunk-UG274TNV.js.map +0 -1
- /package/dist/{chunk-HBOPSFQQ.js.map → chunk-EVXI2I6G.js.map} +0 -0
- /package/dist/{chunk-4DLFJJOQ.js.map → chunk-QMN3CIFS.js.map} +0 -0
package/dist/connectors/index.js
CHANGED
|
@@ -22,8 +22,8 @@ import {
|
|
|
22
22
|
upsertHermesConfig,
|
|
23
23
|
validateMarketplaceManifest,
|
|
24
24
|
writeMarketplaceManifest
|
|
25
|
-
} from "../chunk-
|
|
26
|
-
import "../chunk-
|
|
25
|
+
} from "../chunk-QMN3CIFS.js";
|
|
26
|
+
import "../chunk-E2SPGGUI.js";
|
|
27
27
|
import {
|
|
28
28
|
runCodexMaterialize
|
|
29
29
|
} from "../chunk-2RLECZ6J.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -71,7 +71,7 @@ export { c as CONNECTOR_ID_PATTERN, C as ConnectorConfig, b as ConnectorCursor,
|
|
|
71
71
|
export { b as CONNECTORS_OUTPUT_FORMATS, c as ConnectorRow, C as ConnectorRunResult, d as ConnectorState, e as ConnectorSyncStatus, a as ConnectorsOutputFormat, P as ParsedConnectorsListOptions, f as ParsedConnectorsStatusOptions, R as RunConnectorPollOnceArgs, l as listConnectorStates, p as parseConnectorsFormat, g as parseConnectorsListOptions, h as parseConnectorsRunName, i as parseConnectorsStatusOptions, r as readConnectorState, j as renderConnectorsList, k as renderConnectorsRunResult, m as runConnectorPollOnce, w as writeConnectorState } from './connectors-cli-2iaQ5tX2.js';
|
|
72
72
|
export { LiveConnectorDefinition, LiveConnectorRunItem, LiveConnectorSkipReason, LiveConnectorsRunSummary, builtInLiveConnectorDefinitions, hasEnabledLiveConnector, runLiveConnectorsOnce } from './live-connectors-runner.js';
|
|
73
73
|
import { TokenEntry } from './tokens.js';
|
|
74
|
-
export { TokenStore, generateToken, getAllValidTokens, getAllValidTokensCached, listTokens, loadTokenStore, resolveConnectorFromToken, revokeToken, saveTokenStore } from './tokens.js';
|
|
74
|
+
export { TokenStore, buildTokenEntry, commitTokenEntry, generateToken, getAllValidTokenEntriesCached, getAllValidTokens, getAllValidTokensCached, listTokens, loadTokenStore, resolveConnectorFromToken, revokeToken, saveTokenStore } from './tokens.js';
|
|
75
75
|
export { PostConsolidationMaterializeOptions, RunMaterializeOptions, runCodexMaterialize, runPostConsolidationMaterialize } from './connectors/codex-materialize-runner.js';
|
|
76
76
|
export { MATERIALIZE_VERSION, MaterializeOptions, MaterializeResult, RolloutSummaryInput, SENTINEL_FILE, describeMemoriesDir, ensureSentinel, materializeForNamespace } from './connectors/codex-materialize.js';
|
|
77
77
|
import { T as Taxonomy, R as ResolverDecision } from './types-DJhqDJUV.js';
|
package/dist/index.js
CHANGED
|
@@ -186,7 +186,7 @@ import {
|
|
|
186
186
|
saveRegistry,
|
|
187
187
|
validateMarketplaceManifest,
|
|
188
188
|
writeMarketplaceManifest
|
|
189
|
-
} from "./chunk-
|
|
189
|
+
} from "./chunk-QMN3CIFS.js";
|
|
190
190
|
import "./chunk-V5OCT34X.js";
|
|
191
191
|
import "./chunk-LXOM6IQU.js";
|
|
192
192
|
import "./chunk-VOUOLGIP.js";
|
|
@@ -196,7 +196,10 @@ import {
|
|
|
196
196
|
} from "./chunk-S75M5ZRK.js";
|
|
197
197
|
import "./chunk-ECZU5BJH.js";
|
|
198
198
|
import {
|
|
199
|
+
buildTokenEntry,
|
|
200
|
+
commitTokenEntry,
|
|
199
201
|
generateToken,
|
|
202
|
+
getAllValidTokenEntriesCached,
|
|
200
203
|
getAllValidTokens,
|
|
201
204
|
getAllValidTokensCached,
|
|
202
205
|
listTokens,
|
|
@@ -204,7 +207,7 @@ import {
|
|
|
204
207
|
resolveConnectorFromToken,
|
|
205
208
|
revokeToken,
|
|
206
209
|
saveTokenStore
|
|
207
|
-
} from "./chunk-
|
|
210
|
+
} from "./chunk-E2SPGGUI.js";
|
|
208
211
|
import "./chunk-UHGBNIOS.js";
|
|
209
212
|
import "./chunk-BCDKBDSY.js";
|
|
210
213
|
import "./chunk-YN2UN7N3.js";
|
|
@@ -344,7 +347,7 @@ import {
|
|
|
344
347
|
registerTrainingExportAdapter,
|
|
345
348
|
runBulkImportCliCommand,
|
|
346
349
|
runWearablesCliCommand
|
|
347
|
-
} from "./chunk-
|
|
350
|
+
} from "./chunk-EVXI2I6G.js";
|
|
348
351
|
import "./chunk-MC4FJXPA.js";
|
|
349
352
|
import "./chunk-LQHDIS7L.js";
|
|
350
353
|
import "./chunk-7F7Z6MOS.js";
|
|
@@ -545,7 +548,7 @@ import "./chunk-H3FZVNRN.js";
|
|
|
545
548
|
import "./chunk-OADWQ5CR.js";
|
|
546
549
|
import {
|
|
547
550
|
EngramAccessHttpServer
|
|
548
|
-
} from "./chunk-
|
|
551
|
+
} from "./chunk-SIPZ5UMK.js";
|
|
549
552
|
import {
|
|
550
553
|
WearablesInputError,
|
|
551
554
|
describeErrorForOperator
|
|
@@ -559,7 +562,7 @@ import "./chunk-42NQ7AVG.js";
|
|
|
559
562
|
import "./chunk-7RXCMVFQ.js";
|
|
560
563
|
import {
|
|
561
564
|
EngramMcpServer
|
|
562
|
-
} from "./chunk-
|
|
565
|
+
} from "./chunk-CNXMWYLA.js";
|
|
563
566
|
import {
|
|
564
567
|
buildCitationGuidance,
|
|
565
568
|
formatOaiMemCitation,
|
|
@@ -1070,6 +1073,7 @@ export {
|
|
|
1070
1073
|
buildResponseGuidanceRecallSection,
|
|
1071
1074
|
buildRetrievedMemoryProvenance,
|
|
1072
1075
|
buildTargetedFactRecallSection,
|
|
1076
|
+
buildTokenEntry,
|
|
1073
1077
|
buildTrajectoryAnalysisRecallSection,
|
|
1074
1078
|
builtInLiveConnectorDefinitions,
|
|
1075
1079
|
checkMarketplaceManifest,
|
|
@@ -1090,6 +1094,7 @@ export {
|
|
|
1090
1094
|
collectQuestionSlotCues,
|
|
1091
1095
|
collectStructuredPlanCues,
|
|
1092
1096
|
collectTemporalLexicalCues,
|
|
1097
|
+
commitTokenEntry,
|
|
1093
1098
|
compileCorrectionRule,
|
|
1094
1099
|
compileCorrectionRules,
|
|
1095
1100
|
compileOfflineSyncExcludeGlobs,
|
|
@@ -1163,6 +1168,7 @@ export {
|
|
|
1163
1168
|
generateToken,
|
|
1164
1169
|
generateWearableMemories,
|
|
1165
1170
|
getActiveSpace,
|
|
1171
|
+
getAllValidTokenEntriesCached,
|
|
1166
1172
|
getAllValidTokens,
|
|
1167
1173
|
getAllValidTokensCached,
|
|
1168
1174
|
getAuditLog,
|
package/dist/orchestrator.js
CHANGED
|
@@ -68,13 +68,13 @@ import "./chunk-ABP6QMVM.js";
|
|
|
68
68
|
import "./chunk-HW5EF6LV.js";
|
|
69
69
|
import "./chunk-QPTP6SKC.js";
|
|
70
70
|
import "./chunk-JRZV3767.js";
|
|
71
|
-
import "./chunk-
|
|
71
|
+
import "./chunk-QMN3CIFS.js";
|
|
72
72
|
import "./chunk-V5OCT34X.js";
|
|
73
73
|
import "./chunk-LXOM6IQU.js";
|
|
74
74
|
import "./chunk-VOUOLGIP.js";
|
|
75
75
|
import "./chunk-S75M5ZRK.js";
|
|
76
76
|
import "./chunk-ECZU5BJH.js";
|
|
77
|
-
import "./chunk-
|
|
77
|
+
import "./chunk-E2SPGGUI.js";
|
|
78
78
|
import "./chunk-UHGBNIOS.js";
|
|
79
79
|
import "./chunk-BCDKBDSY.js";
|
|
80
80
|
import "./chunk-YN2UN7N3.js";
|
|
@@ -125,7 +125,7 @@ import "./chunk-LUPVCGYK.js";
|
|
|
125
125
|
import "./chunk-PYPOFEMK.js";
|
|
126
126
|
import "./chunk-LOPZ2DWR.js";
|
|
127
127
|
import "./chunk-VEWZZM3H.js";
|
|
128
|
-
import "./chunk-
|
|
128
|
+
import "./chunk-EVXI2I6G.js";
|
|
129
129
|
import "./chunk-MC4FJXPA.js";
|
|
130
130
|
import "./chunk-LQHDIS7L.js";
|
|
131
131
|
import "./chunk-7F7Z6MOS.js";
|
|
@@ -216,7 +216,7 @@ import "./chunk-KD3QD3A5.js";
|
|
|
216
216
|
import "./chunk-VGUOEDTU.js";
|
|
217
217
|
import "./chunk-H3FZVNRN.js";
|
|
218
218
|
import "./chunk-OADWQ5CR.js";
|
|
219
|
-
import "./chunk-
|
|
219
|
+
import "./chunk-SIPZ5UMK.js";
|
|
220
220
|
import "./chunk-7WV3F5DQ.js";
|
|
221
221
|
import "./chunk-SEDEKFYQ.js";
|
|
222
222
|
import "./chunk-JBPKEARU.js";
|
|
@@ -225,7 +225,7 @@ import "./chunk-J64TK33U.js";
|
|
|
225
225
|
import "./chunk-RSUYKGGZ.js";
|
|
226
226
|
import "./chunk-42NQ7AVG.js";
|
|
227
227
|
import "./chunk-7RXCMVFQ.js";
|
|
228
|
-
import "./chunk-
|
|
228
|
+
import "./chunk-CNXMWYLA.js";
|
|
229
229
|
import "./chunk-D24OXEPB.js";
|
|
230
230
|
import "./chunk-4JG55YV3.js";
|
|
231
231
|
import "./chunk-GLERZDM7.js";
|
package/dist/tokens.d.ts
CHANGED
|
@@ -38,7 +38,9 @@ declare function generateToken(connector: string, tokensPath?: string): TokenEnt
|
|
|
38
38
|
declare function listTokens(tokensPath?: string): TokenEntry[];
|
|
39
39
|
declare function revokeToken(connector: string, tokensPath?: string): boolean;
|
|
40
40
|
declare function getAllValidTokens(tokensPath?: string): string[];
|
|
41
|
+
/** Cached token-entry snapshot ({token, connector} pairs). */
|
|
42
|
+
declare function getAllValidTokenEntriesCached(tokensPath?: string): readonly TokenEntry[];
|
|
41
43
|
declare function getAllValidTokensCached(tokensPath?: string): string[];
|
|
42
44
|
declare function resolveConnectorFromToken(token: string, tokensPath?: string): string | undefined;
|
|
43
45
|
|
|
44
|
-
export { type TokenEntry, type TokenStore, buildTokenEntry, commitTokenEntry, generateToken, getAllValidTokens, getAllValidTokensCached, listTokens, loadTokenStore, resolveConnectorFromToken, revokeToken, saveTokenStore };
|
|
46
|
+
export { type TokenEntry, type TokenStore, buildTokenEntry, commitTokenEntry, generateToken, getAllValidTokenEntriesCached, getAllValidTokens, getAllValidTokensCached, listTokens, loadTokenStore, resolveConnectorFromToken, revokeToken, saveTokenStore };
|
package/dist/tokens.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
buildTokenEntry,
|
|
3
3
|
commitTokenEntry,
|
|
4
4
|
generateToken,
|
|
5
|
+
getAllValidTokenEntriesCached,
|
|
5
6
|
getAllValidTokens,
|
|
6
7
|
getAllValidTokensCached,
|
|
7
8
|
listTokens,
|
|
@@ -9,13 +10,14 @@ import {
|
|
|
9
10
|
resolveConnectorFromToken,
|
|
10
11
|
revokeToken,
|
|
11
12
|
saveTokenStore
|
|
12
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-E2SPGGUI.js";
|
|
13
14
|
import "./chunk-JUC24CTX.js";
|
|
14
15
|
import "./chunk-PZ5AY32C.js";
|
|
15
16
|
export {
|
|
16
17
|
buildTokenEntry,
|
|
17
18
|
commitTokenEntry,
|
|
18
19
|
generateToken,
|
|
20
|
+
getAllValidTokenEntriesCached,
|
|
19
21
|
getAllValidTokens,
|
|
20
22
|
getAllValidTokensCached,
|
|
21
23
|
listTokens,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/core",
|
|
3
|
-
"version": "9.3.
|
|
3
|
+
"version": "9.3.769",
|
|
4
4
|
"description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -3000,7 +3000,7 @@
|
|
|
3000
3000
|
"core"
|
|
3001
3001
|
],
|
|
3002
3002
|
"peerDependencies": {
|
|
3003
|
-
"@remnic/coding-graph": "^9.3.
|
|
3003
|
+
"@remnic/coding-graph": "^9.3.769"
|
|
3004
3004
|
},
|
|
3005
3005
|
"peerDependenciesMeta": {
|
|
3006
3006
|
"@remnic/coding-graph": {
|
package/src/access-http.test.ts
CHANGED
|
@@ -1454,3 +1454,353 @@ test("HTTP offline apply requires a changeset", async () => {
|
|
|
1454
1454
|
await server.stop();
|
|
1455
1455
|
}
|
|
1456
1456
|
});
|
|
1457
|
+
|
|
1458
|
+
test("HTTP server rejects invalid resourceMetadataUrl at construction", () => {
|
|
1459
|
+
const service = {} as EngramAccessService;
|
|
1460
|
+
for (const bad of ["not a url", "ftp://example.com/oauth", "//relative/path", ""]) {
|
|
1461
|
+
assert.throws(
|
|
1462
|
+
() =>
|
|
1463
|
+
new EngramAccessHttpServer({
|
|
1464
|
+
service,
|
|
1465
|
+
port: 0,
|
|
1466
|
+
authToken: "test-token",
|
|
1467
|
+
adminConsoleEnabled: false,
|
|
1468
|
+
resourceMetadataUrl: bad,
|
|
1469
|
+
}),
|
|
1470
|
+
/access HTTP resourceMetadataUrl/,
|
|
1471
|
+
`resourceMetadataUrl=${JSON.stringify(bad)} must be rejected`,
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
// http and https are accepted.
|
|
1475
|
+
for (const ok of [
|
|
1476
|
+
"https://example.com/.well-known/oauth-protected-resource",
|
|
1477
|
+
"http://127.0.0.1:8787/.well-known/oauth-protected-resource",
|
|
1478
|
+
]) {
|
|
1479
|
+
const server = new EngramAccessHttpServer({
|
|
1480
|
+
service,
|
|
1481
|
+
port: 0,
|
|
1482
|
+
authToken: "test-token",
|
|
1483
|
+
adminConsoleEnabled: false,
|
|
1484
|
+
resourceMetadataUrl: ok,
|
|
1485
|
+
});
|
|
1486
|
+
assert.ok(server, `resourceMetadataUrl=${ok} should be accepted`);
|
|
1487
|
+
}
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
test("HTTP 401 www-authenticate carries resource_metadata exactly when configured", async () => {
|
|
1491
|
+
const service = {} as EngramAccessService;
|
|
1492
|
+
const metadataUrl = "https://example.test/.well-known/oauth-protected-resource";
|
|
1493
|
+
const server = new EngramAccessHttpServer({
|
|
1494
|
+
service,
|
|
1495
|
+
port: 0,
|
|
1496
|
+
authToken: "test-token",
|
|
1497
|
+
adminConsoleEnabled: false,
|
|
1498
|
+
resourceMetadataUrl: metadataUrl,
|
|
1499
|
+
});
|
|
1500
|
+
const status = await server.start();
|
|
1501
|
+
try {
|
|
1502
|
+
const response = await fetch(`http://127.0.0.1:${status.port}/engram/v1/health`);
|
|
1503
|
+
assert.equal(response.status, 401);
|
|
1504
|
+
assert.equal(
|
|
1505
|
+
response.headers.get("www-authenticate"),
|
|
1506
|
+
`Bearer resource_metadata="${metadataUrl}"`,
|
|
1507
|
+
);
|
|
1508
|
+
} finally {
|
|
1509
|
+
await server.stop();
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
test("HTTP 401 www-authenticate is the bare Bearer challenge when resourceMetadataUrl is unset", async () => {
|
|
1514
|
+
const service = {} as EngramAccessService;
|
|
1515
|
+
const server = new EngramAccessHttpServer({
|
|
1516
|
+
service,
|
|
1517
|
+
port: 0,
|
|
1518
|
+
authToken: "test-token",
|
|
1519
|
+
adminConsoleEnabled: false,
|
|
1520
|
+
});
|
|
1521
|
+
const status = await server.start();
|
|
1522
|
+
try {
|
|
1523
|
+
const response = await fetch(`http://127.0.0.1:${status.port}/engram/v1/health`);
|
|
1524
|
+
assert.equal(response.status, 401);
|
|
1525
|
+
assert.equal(response.headers.get("www-authenticate"), "Bearer");
|
|
1526
|
+
} finally {
|
|
1527
|
+
await server.stop();
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
test("HTTP /mcp returns 405 with Allow: POST for GET and DELETE (authorized requests)", async () => {
|
|
1532
|
+
const service = {} as EngramAccessService;
|
|
1533
|
+
const server = new EngramAccessHttpServer({
|
|
1534
|
+
service,
|
|
1535
|
+
port: 0,
|
|
1536
|
+
authToken: "test-token",
|
|
1537
|
+
adminConsoleEnabled: false,
|
|
1538
|
+
});
|
|
1539
|
+
const status = await server.start();
|
|
1540
|
+
try {
|
|
1541
|
+
for (const method of ["GET", "DELETE"]) {
|
|
1542
|
+
const response = await fetch(`http://127.0.0.1:${status.port}/mcp`, {
|
|
1543
|
+
method,
|
|
1544
|
+
headers: { authorization: "Bearer test-token" },
|
|
1545
|
+
});
|
|
1546
|
+
assert.equal(response.status, 405, `${method} /mcp must be 405`);
|
|
1547
|
+
assert.equal(response.headers.get("allow"), "POST", `${method} /mcp must advertise Allow: POST`);
|
|
1548
|
+
const body = await response.json() as { code?: string };
|
|
1549
|
+
assert.equal(body.code, "method_not_allowed");
|
|
1550
|
+
}
|
|
1551
|
+
// Unauthenticated GET /mcp still gets 401 first (auth gate beats method-conformance).
|
|
1552
|
+
const unauth = await fetch(`http://127.0.0.1:${status.port}/mcp`, { method: "GET" });
|
|
1553
|
+
assert.equal(unauth.status, 401);
|
|
1554
|
+
} finally {
|
|
1555
|
+
await server.stop();
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
test("HTTP /mcp rejects unknown MCP-Protocol-Version header with 400 JSON-RPC error", async () => {
|
|
1560
|
+
const service = {} as EngramAccessService;
|
|
1561
|
+
const server = new EngramAccessHttpServer({
|
|
1562
|
+
service,
|
|
1563
|
+
port: 0,
|
|
1564
|
+
authToken: "test-token",
|
|
1565
|
+
adminConsoleEnabled: false,
|
|
1566
|
+
});
|
|
1567
|
+
const status = await server.start();
|
|
1568
|
+
try {
|
|
1569
|
+
const response = await fetch(`http://127.0.0.1:${status.port}/mcp`, {
|
|
1570
|
+
method: "POST",
|
|
1571
|
+
headers: {
|
|
1572
|
+
authorization: "Bearer test-token",
|
|
1573
|
+
"content-type": "application/json",
|
|
1574
|
+
"mcp-protocol-version": "1999-01-01",
|
|
1575
|
+
},
|
|
1576
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }),
|
|
1577
|
+
});
|
|
1578
|
+
assert.equal(response.status, 400);
|
|
1579
|
+
const body = await response.json() as { jsonrpc?: string; error?: { message?: string } };
|
|
1580
|
+
assert.equal(body.jsonrpc, "2.0");
|
|
1581
|
+
assert.match(body.error?.message ?? "", /unsupported MCP-Protocol-Version/);
|
|
1582
|
+
|
|
1583
|
+
// A supported header is accepted (and a valid request proceeds normally).
|
|
1584
|
+
for (const v of ["2025-06-18", "2025-03-26", "2024-11-05"]) {
|
|
1585
|
+
const ok = await fetch(`http://127.0.0.1:${status.port}/mcp`, {
|
|
1586
|
+
method: "POST",
|
|
1587
|
+
headers: {
|
|
1588
|
+
authorization: "Bearer test-token",
|
|
1589
|
+
"content-type": "application/json",
|
|
1590
|
+
"mcp-protocol-version": v,
|
|
1591
|
+
},
|
|
1592
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }),
|
|
1593
|
+
});
|
|
1594
|
+
assert.equal(ok.status, 200, `version ${v} should be accepted`);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// Absent header is also fine.
|
|
1598
|
+
const absent = await fetch(`http://127.0.0.1:${status.port}/mcp`, {
|
|
1599
|
+
method: "POST",
|
|
1600
|
+
headers: { authorization: "Bearer test-token", "content-type": "application/json" },
|
|
1601
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }),
|
|
1602
|
+
});
|
|
1603
|
+
assert.equal(absent.status, 200);
|
|
1604
|
+
} finally {
|
|
1605
|
+
await server.stop();
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
test("HTTP externalRequestHandler runs pre-auth, can end responses, and falls through on false", async () => {
|
|
1610
|
+
// Minimal service stub: the fall-through leg hits /engram/v1/health, which
|
|
1611
|
+
// calls service.health(). The stub is signature-faithful (full
|
|
1612
|
+
// EngramAccessHealthResponse via `satisfies`) so interface drift fails here
|
|
1613
|
+
// instead of passing vacuously; everything else in this test bypasses the
|
|
1614
|
+
// service.
|
|
1615
|
+
const healthStub = {
|
|
1616
|
+
health: async () => ({
|
|
1617
|
+
ok: true as const,
|
|
1618
|
+
memoryDir: "/tmp/remnic-test",
|
|
1619
|
+
namespacesEnabled: false,
|
|
1620
|
+
defaultNamespace: "default",
|
|
1621
|
+
searchBackend: "recent",
|
|
1622
|
+
qmdEnabled: false,
|
|
1623
|
+
qmd: {
|
|
1624
|
+
enabled: false,
|
|
1625
|
+
active: false,
|
|
1626
|
+
degraded: false,
|
|
1627
|
+
mode: "disabled" as const,
|
|
1628
|
+
collection: "",
|
|
1629
|
+
collectionState: "skipped" as const,
|
|
1630
|
+
installedVersion: null,
|
|
1631
|
+
supportedVersion: null,
|
|
1632
|
+
supported: null,
|
|
1633
|
+
upgradeAvailable: null,
|
|
1634
|
+
doctorAvailable: null,
|
|
1635
|
+
debugStatus: "disabled",
|
|
1636
|
+
},
|
|
1637
|
+
nativeKnowledgeEnabled: false,
|
|
1638
|
+
projectionAvailable: false,
|
|
1639
|
+
}),
|
|
1640
|
+
} satisfies Pick<EngramAccessService, "health">;
|
|
1641
|
+
const service = healthStub as EngramAccessService;
|
|
1642
|
+
const seen: Array<{ path: string; method: string; authorized: boolean }> = [];
|
|
1643
|
+
const server = new EngramAccessHttpServer({
|
|
1644
|
+
service,
|
|
1645
|
+
port: 0,
|
|
1646
|
+
authToken: "test-token",
|
|
1647
|
+
adminConsoleEnabled: false,
|
|
1648
|
+
externalRequestHandler: async (_req, res, ctx) => {
|
|
1649
|
+
seen.push({
|
|
1650
|
+
path: new URL(_req.url ?? "/", "http://placeholder").pathname,
|
|
1651
|
+
method: _req.method ?? "",
|
|
1652
|
+
authorized: ctx.authorized,
|
|
1653
|
+
});
|
|
1654
|
+
if (new URL(_req.url ?? "/", "http://placeholder").pathname === "/probe/handled") {
|
|
1655
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1656
|
+
res.end(JSON.stringify({ handled: true, authorized: ctx.authorized }));
|
|
1657
|
+
return true;
|
|
1658
|
+
}
|
|
1659
|
+
return false; // fall through to the normal pipeline
|
|
1660
|
+
},
|
|
1661
|
+
});
|
|
1662
|
+
const status = await server.start();
|
|
1663
|
+
try {
|
|
1664
|
+
// Pre-auth, the handler sees authorized=false (no token sent).
|
|
1665
|
+
const handled = await fetch(`http://127.0.0.1:${status.port}/probe/handled`);
|
|
1666
|
+
assert.equal(handled.status, 200);
|
|
1667
|
+
const handledBody = await handled.json() as { handled?: boolean; authorized?: boolean };
|
|
1668
|
+
assert.equal(handledBody.handled, true);
|
|
1669
|
+
assert.equal(handledBody.authorized, false, "handler must observe authorized=false pre-token");
|
|
1670
|
+
|
|
1671
|
+
// Fall-through path: same handler returns false, request continues to normal
|
|
1672
|
+
// routing. Hit a real endpoint so we know the request reached it.
|
|
1673
|
+
const passthrough = await fetch(
|
|
1674
|
+
`http://127.0.0.1:${status.port}/engram/v1/health`,
|
|
1675
|
+
{ headers: { authorization: "Bearer test-token" } },
|
|
1676
|
+
);
|
|
1677
|
+
assert.equal(passthrough.status, 200, "fall-through should reach the normal health route");
|
|
1678
|
+
const passthroughBody = await passthrough.json() as { ok?: boolean; memoryDir?: string };
|
|
1679
|
+
assert.equal(passthroughBody.ok, true, "fall-through must return the stubbed health payload");
|
|
1680
|
+
assert.equal(passthroughBody.memoryDir, "/tmp/remnic-test");
|
|
1681
|
+
|
|
1682
|
+
// Authorized request: handler sees ctx.authorized=true.
|
|
1683
|
+
const authed = await fetch(
|
|
1684
|
+
`http://127.0.0.1:${status.port}/probe/handled`,
|
|
1685
|
+
{ headers: { authorization: "Bearer test-token" } },
|
|
1686
|
+
);
|
|
1687
|
+
assert.equal(authed.status, 200);
|
|
1688
|
+
const authedBody = await authed.json() as { authorized?: boolean };
|
|
1689
|
+
assert.equal(authedBody.authorized, true, "handler must observe authorized=true with valid token");
|
|
1690
|
+
|
|
1691
|
+
assert.deepEqual(seen, [
|
|
1692
|
+
{ path: "/probe/handled", method: "GET", authorized: false },
|
|
1693
|
+
{ path: "/engram/v1/health", method: "GET", authorized: true },
|
|
1694
|
+
{ path: "/probe/handled", method: "GET", authorized: true },
|
|
1695
|
+
]);
|
|
1696
|
+
} finally {
|
|
1697
|
+
await server.stop();
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
test("HTTP externalRequestHandler errors flow through the existing error handler", async () => {
|
|
1702
|
+
const service = {} as EngramAccessService;
|
|
1703
|
+
const server = new EngramAccessHttpServer({
|
|
1704
|
+
service,
|
|
1705
|
+
port: 0,
|
|
1706
|
+
authToken: "test-token",
|
|
1707
|
+
adminConsoleEnabled: false,
|
|
1708
|
+
externalRequestHandler: async () => {
|
|
1709
|
+
throw new Error("external-handler-explosion");
|
|
1710
|
+
},
|
|
1711
|
+
});
|
|
1712
|
+
const status = await server.start();
|
|
1713
|
+
try {
|
|
1714
|
+
const response = await fetch(`http://127.0.0.1:${status.port}/engram/v1/health`);
|
|
1715
|
+
assert.equal(response.status, 500, "thrown errors must produce a 500 via the existing error handler");
|
|
1716
|
+
const body = await response.json() as { code?: string };
|
|
1717
|
+
assert.equal(body.code, "internal_error");
|
|
1718
|
+
} finally {
|
|
1719
|
+
await server.stop();
|
|
1720
|
+
}
|
|
1721
|
+
});
|
|
1722
|
+
|
|
1723
|
+
test("HTTP authTokenEntriesGetter is authoritative: scope policy binds connectors and never falls through", async () => {
|
|
1724
|
+
// Signature-faithful health stub so non-MCP authorization outcomes are
|
|
1725
|
+
// observable as 200-with-body (a bare `{}` service would 500 and mask
|
|
1726
|
+
// accidental policy application).
|
|
1727
|
+
const healthStub = {
|
|
1728
|
+
health: async () => ({
|
|
1729
|
+
ok: true as const,
|
|
1730
|
+
memoryDir: "/tmp/remnic-scope-test",
|
|
1731
|
+
namespacesEnabled: false,
|
|
1732
|
+
defaultNamespace: "default",
|
|
1733
|
+
searchBackend: "recent",
|
|
1734
|
+
qmdEnabled: false,
|
|
1735
|
+
qmd: {
|
|
1736
|
+
enabled: false,
|
|
1737
|
+
active: false,
|
|
1738
|
+
degraded: false,
|
|
1739
|
+
mode: "disabled" as const,
|
|
1740
|
+
collection: "",
|
|
1741
|
+
collectionState: "skipped" as const,
|
|
1742
|
+
installedVersion: null,
|
|
1743
|
+
supportedVersion: null,
|
|
1744
|
+
supported: null,
|
|
1745
|
+
upgradeAvailable: null,
|
|
1746
|
+
doctorAvailable: null,
|
|
1747
|
+
debugStatus: "disabled",
|
|
1748
|
+
},
|
|
1749
|
+
nativeKnowledgeEnabled: false,
|
|
1750
|
+
projectionAvailable: false,
|
|
1751
|
+
}),
|
|
1752
|
+
} satisfies Pick<EngramAccessService, "health">;
|
|
1753
|
+
const entries = [
|
|
1754
|
+
{ token: "remnic_cg_scoped", connector: "chatgpt" },
|
|
1755
|
+
{ token: "remnic_cx_free", connector: "codex" },
|
|
1756
|
+
{ token: "remnic_xx_anon" }, // no connector — must fail closed under a policy
|
|
1757
|
+
];
|
|
1758
|
+
const server = new EngramAccessHttpServer({
|
|
1759
|
+
service: healthStub as EngramAccessService,
|
|
1760
|
+
port: 0,
|
|
1761
|
+
authToken: "operator-token",
|
|
1762
|
+
// Dangerous shape on purpose: BOTH getters configured, and the string
|
|
1763
|
+
// getter is a superset (extra "string_only_token"). The entries getter
|
|
1764
|
+
// must decide alone; nothing may leak into the string getter.
|
|
1765
|
+
authTokensGetter: () => [...entries.map((entry) => entry.token), "string_only_token"],
|
|
1766
|
+
authTokenEntriesGetter: () => entries,
|
|
1767
|
+
tokenPathPolicy: (connector, pathname) => connector !== "chatgpt" || pathname === "/mcp",
|
|
1768
|
+
adminConsoleEnabled: false,
|
|
1769
|
+
});
|
|
1770
|
+
const status = await server.start();
|
|
1771
|
+
const request = (token: string, path: string, method = "GET") =>
|
|
1772
|
+
fetch(`http://127.0.0.1:${status.port}${path}`, {
|
|
1773
|
+
method,
|
|
1774
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1775
|
+
...(method === "POST" ? { body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }) } : {}),
|
|
1776
|
+
});
|
|
1777
|
+
try {
|
|
1778
|
+
// Scoped chatgpt token: /mcp only; health denied.
|
|
1779
|
+
assert.equal((await request("remnic_cg_scoped", "/mcp", "POST")).status, 200);
|
|
1780
|
+
assert.equal(
|
|
1781
|
+
(await request("remnic_cg_scoped", "/engram/v1/health")).status,
|
|
1782
|
+
401,
|
|
1783
|
+
"chatgpt token must be denied off /mcp even with a permissive string getter present",
|
|
1784
|
+
);
|
|
1785
|
+
// Other connector tokens are unrestricted by this policy: /mcp AND health.
|
|
1786
|
+
assert.equal((await request("remnic_cx_free", "/mcp", "POST")).status, 200);
|
|
1787
|
+
const codexHealth = await request("remnic_cx_free", "/engram/v1/health");
|
|
1788
|
+
assert.equal(codexHealth.status, 200);
|
|
1789
|
+
assert.equal(((await codexHealth.json()) as { ok?: boolean }).ok, true);
|
|
1790
|
+
// Entry without connector fails closed when a policy is configured.
|
|
1791
|
+
assert.equal((await request("remnic_xx_anon", "/mcp", "POST")).status, 401);
|
|
1792
|
+
// A token present ONLY in the string getter is NOT honored: the entries
|
|
1793
|
+
// getter is authoritative and there is no fall-through.
|
|
1794
|
+
assert.equal((await request("string_only_token", "/mcp", "POST")).status, 401);
|
|
1795
|
+
assert.equal((await request("string_only_token", "/engram/v1/health")).status, 401);
|
|
1796
|
+
// Unknown tokens are rejected everywhere.
|
|
1797
|
+
assert.equal((await request("remnic_zz_unknown", "/mcp", "POST")).status, 401);
|
|
1798
|
+
// Static operator token bypasses the policy entirely: /mcp AND health.
|
|
1799
|
+
assert.equal((await request("operator-token", "/mcp", "POST")).status, 200);
|
|
1800
|
+
const operatorHealth = await request("operator-token", "/engram/v1/health");
|
|
1801
|
+
assert.equal(operatorHealth.status, 200);
|
|
1802
|
+
assert.equal(((await operatorHealth.json()) as { ok?: boolean }).ok, true);
|
|
1803
|
+
} finally {
|
|
1804
|
+
await server.stop();
|
|
1805
|
+
}
|
|
1806
|
+
});
|