@labacacia/nps-sdk 1.0.0-alpha.1
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/CONTRIBUTING.md +33 -0
- package/LICENSE +170 -0
- package/NOTICE +7 -0
- package/README.md +153 -0
- package/dist/codec-CmHeovTV.d.cts +120 -0
- package/dist/codec-CmHeovTV.d.ts +120 -0
- package/dist/core/anchor-cache.d.ts +42 -0
- package/dist/core/anchor-cache.d.ts.map +1 -0
- package/dist/core/anchor-cache.js +104 -0
- package/dist/core/anchor-cache.js.map +1 -0
- package/dist/core/cache.d.ts +14 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +80 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/canonical-json.d.ts +12 -0
- package/dist/core/canonical-json.d.ts.map +1 -0
- package/dist/core/canonical-json.js +44 -0
- package/dist/core/canonical-json.js.map +1 -0
- package/dist/core/codec.d.ts +32 -0
- package/dist/core/codec.d.ts.map +1 -0
- package/dist/core/codec.js +119 -0
- package/dist/core/codec.js.map +1 -0
- package/dist/core/codecs/index.d.ts +4 -0
- package/dist/core/codecs/index.d.ts.map +1 -0
- package/dist/core/codecs/index.js +6 -0
- package/dist/core/codecs/index.js.map +1 -0
- package/dist/core/codecs/ncp-codec.d.ts +39 -0
- package/dist/core/codecs/ncp-codec.d.ts.map +1 -0
- package/dist/core/codecs/ncp-codec.js +93 -0
- package/dist/core/codecs/ncp-codec.js.map +1 -0
- package/dist/core/codecs/tier1-json-codec.d.ts +10 -0
- package/dist/core/codecs/tier1-json-codec.d.ts.map +1 -0
- package/dist/core/codecs/tier1-json-codec.js +28 -0
- package/dist/core/codecs/tier1-json-codec.js.map +1 -0
- package/dist/core/codecs/tier2-msgpack-codec.d.ts +10 -0
- package/dist/core/codecs/tier2-msgpack-codec.d.ts.map +1 -0
- package/dist/core/codecs/tier2-msgpack-codec.js +26 -0
- package/dist/core/codecs/tier2-msgpack-codec.js.map +1 -0
- package/dist/core/crypto-provider.d.ts +31 -0
- package/dist/core/crypto-provider.d.ts.map +1 -0
- package/dist/core/crypto-provider.js +10 -0
- package/dist/core/crypto-provider.js.map +1 -0
- package/dist/core/exceptions.d.ts +27 -0
- package/dist/core/exceptions.d.ts.map +1 -0
- package/dist/core/exceptions.js +52 -0
- package/dist/core/exceptions.js.map +1 -0
- package/dist/core/frame-header.d.ts +87 -0
- package/dist/core/frame-header.d.ts.map +1 -0
- package/dist/core/frame-header.js +185 -0
- package/dist/core/frame-header.js.map +1 -0
- package/dist/core/frame-registry.d.ts +35 -0
- package/dist/core/frame-registry.d.ts.map +1 -0
- package/dist/core/frame-registry.js +63 -0
- package/dist/core/frame-registry.js.map +1 -0
- package/dist/core/frames.d.ts +80 -0
- package/dist/core/frames.d.ts.map +1 -0
- package/dist/core/frames.js +153 -0
- package/dist/core/frames.js.map +1 -0
- package/dist/core/index.cjs +371 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +41 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +10 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/registry.d.ts +11 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +17 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/status-codes.d.ts +28 -0
- package/dist/core/status-codes.d.ts.map +1 -0
- package/dist/core/status-codes.js +38 -0
- package/dist/core/status-codes.js.map +1 -0
- package/dist/frames-B3qLdl_g.d.cts +77 -0
- package/dist/frames-Ff7-ZPUl.d.ts +77 -0
- package/dist/index.cjs +1556 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/ncp/frames/anchor-frame.d.ts +29 -0
- package/dist/ncp/frames/anchor-frame.d.ts.map +1 -0
- package/dist/ncp/frames/anchor-frame.js +54 -0
- package/dist/ncp/frames/anchor-frame.js.map +1 -0
- package/dist/ncp/frames/caps-frame.d.ts +29 -0
- package/dist/ncp/frames/caps-frame.d.ts.map +1 -0
- package/dist/ncp/frames/caps-frame.js +29 -0
- package/dist/ncp/frames/caps-frame.js.map +1 -0
- package/dist/ncp/frames/diff-frame.d.ts +32 -0
- package/dist/ncp/frames/diff-frame.d.ts.map +1 -0
- package/dist/ncp/frames/diff-frame.js +37 -0
- package/dist/ncp/frames/diff-frame.js.map +1 -0
- package/dist/ncp/frames/error-frame.d.ts +16 -0
- package/dist/ncp/frames/error-frame.d.ts.map +1 -0
- package/dist/ncp/frames/error-frame.js +13 -0
- package/dist/ncp/frames/error-frame.js.map +1 -0
- package/dist/ncp/frames/hello-frame.d.ts +21 -0
- package/dist/ncp/frames/hello-frame.d.ts.map +1 -0
- package/dist/ncp/frames/hello-frame.js +25 -0
- package/dist/ncp/frames/hello-frame.js.map +1 -0
- package/dist/ncp/frames/stream-frame.d.ts +16 -0
- package/dist/ncp/frames/stream-frame.d.ts.map +1 -0
- package/dist/ncp/frames/stream-frame.js +18 -0
- package/dist/ncp/frames/stream-frame.js.map +1 -0
- package/dist/ncp/frames.d.ts +76 -0
- package/dist/ncp/frames.d.ts.map +1 -0
- package/dist/ncp/frames.js +147 -0
- package/dist/ncp/frames.js.map +1 -0
- package/dist/ncp/handshake.d.ts +30 -0
- package/dist/ncp/handshake.d.ts.map +1 -0
- package/dist/ncp/handshake.js +80 -0
- package/dist/ncp/handshake.js.map +1 -0
- package/dist/ncp/index.cjs +188 -0
- package/dist/ncp/index.cjs.map +1 -0
- package/dist/ncp/index.d.cts +6 -0
- package/dist/ncp/index.d.ts +11 -0
- package/dist/ncp/index.d.ts.map +1 -0
- package/dist/ncp/index.js +13 -0
- package/dist/ncp/index.js.map +1 -0
- package/dist/ncp/ncp-error-codes.d.ts +22 -0
- package/dist/ncp/ncp-error-codes.d.ts.map +1 -0
- package/dist/ncp/ncp-error-codes.js +32 -0
- package/dist/ncp/ncp-error-codes.js.map +1 -0
- package/dist/ncp/ncp-patch-format.d.ts +7 -0
- package/dist/ncp/ncp-patch-format.d.ts.map +1 -0
- package/dist/ncp/ncp-patch-format.js +13 -0
- package/dist/ncp/ncp-patch-format.js.map +1 -0
- package/dist/ncp/registry.d.ts +3 -0
- package/dist/ncp/registry.d.ts.map +1 -0
- package/dist/ncp/registry.js +12 -0
- package/dist/ncp/registry.js.map +1 -0
- package/dist/ncp/stream-manager.d.ts +57 -0
- package/dist/ncp/stream-manager.d.ts.map +1 -0
- package/dist/ncp/stream-manager.js +163 -0
- package/dist/ncp/stream-manager.js.map +1 -0
- package/dist/ndp/frames.d.ts +56 -0
- package/dist/ndp/frames.d.ts.map +1 -0
- package/dist/ndp/frames.js +87 -0
- package/dist/ndp/frames.js.map +1 -0
- package/dist/ndp/index.cjs +252 -0
- package/dist/ndp/index.cjs.map +1 -0
- package/dist/ndp/index.d.cts +86 -0
- package/dist/ndp/index.d.ts +5 -0
- package/dist/ndp/index.d.ts.map +1 -0
- package/dist/ndp/index.js +7 -0
- package/dist/ndp/index.js.map +1 -0
- package/dist/ndp/ndp-registry.d.ts +11 -0
- package/dist/ndp/ndp-registry.d.ts.map +1 -0
- package/dist/ndp/ndp-registry.js +79 -0
- package/dist/ndp/ndp-registry.js.map +1 -0
- package/dist/ndp/registry.d.ts +3 -0
- package/dist/ndp/registry.d.ts.map +1 -0
- package/dist/ndp/registry.js +10 -0
- package/dist/ndp/registry.js.map +1 -0
- package/dist/ndp/validator.d.ts +18 -0
- package/dist/ndp/validator.d.ts.map +1 -0
- package/dist/ndp/validator.js +48 -0
- package/dist/ndp/validator.js.map +1 -0
- package/dist/nip/frames.d.ts +44 -0
- package/dist/nip/frames.d.ts.map +1 -0
- package/dist/nip/frames.js +81 -0
- package/dist/nip/frames.js.map +1 -0
- package/dist/nip/identity.d.ts +18 -0
- package/dist/nip/identity.d.ts.map +1 -0
- package/dist/nip/identity.js +94 -0
- package/dist/nip/identity.js.map +1 -0
- package/dist/nip/index.cjs +214 -0
- package/dist/nip/index.cjs.map +1 -0
- package/dist/nip/index.d.cts +65 -0
- package/dist/nip/index.d.ts +4 -0
- package/dist/nip/index.d.ts.map +1 -0
- package/dist/nip/index.js +6 -0
- package/dist/nip/index.js.map +1 -0
- package/dist/nip/registry.d.ts +3 -0
- package/dist/nip/registry.d.ts.map +1 -0
- package/dist/nip/registry.js +10 -0
- package/dist/nip/registry.js.map +1 -0
- package/dist/nop/client.d.ts +34 -0
- package/dist/nop/client.d.ts.map +1 -0
- package/dist/nop/client.js +90 -0
- package/dist/nop/client.js.map +1 -0
- package/dist/nop/frames.d.ts +65 -0
- package/dist/nop/frames.d.ts.map +1 -0
- package/dist/nop/frames.js +148 -0
- package/dist/nop/frames.js.map +1 -0
- package/dist/nop/index.cjs +762 -0
- package/dist/nop/index.cjs.map +1 -0
- package/dist/nop/index.d.cts +155 -0
- package/dist/nop/index.d.ts +5 -0
- package/dist/nop/index.d.ts.map +1 -0
- package/dist/nop/index.js +7 -0
- package/dist/nop/index.js.map +1 -0
- package/dist/nop/models.d.ts +58 -0
- package/dist/nop/models.d.ts.map +1 -0
- package/dist/nop/models.js +50 -0
- package/dist/nop/models.js.map +1 -0
- package/dist/nop/nop-types.d.ts +136 -0
- package/dist/nop/nop-types.d.ts.map +1 -0
- package/dist/nop/nop-types.js +44 -0
- package/dist/nop/nop-types.js.map +1 -0
- package/dist/nop/registry.d.ts +3 -0
- package/dist/nop/registry.d.ts.map +1 -0
- package/dist/nop/registry.js +11 -0
- package/dist/nop/registry.js.map +1 -0
- package/dist/nwp/client.d.ts +22 -0
- package/dist/nwp/client.d.ts.map +1 -0
- package/dist/nwp/client.js +101 -0
- package/dist/nwp/client.js.map +1 -0
- package/dist/nwp/frames.d.ts +46 -0
- package/dist/nwp/frames.d.ts.map +1 -0
- package/dist/nwp/frames.js +81 -0
- package/dist/nwp/frames.js.map +1 -0
- package/dist/nwp/index.cjs +658 -0
- package/dist/nwp/index.cjs.map +1 -0
- package/dist/nwp/index.d.cts +65 -0
- package/dist/nwp/index.d.ts +4 -0
- package/dist/nwp/index.d.ts.map +1 -0
- package/dist/nwp/index.js +6 -0
- package/dist/nwp/index.js.map +1 -0
- package/dist/nwp/registry.d.ts +3 -0
- package/dist/nwp/registry.d.ts.map +1 -0
- package/dist/nwp/registry.js +9 -0
- package/dist/nwp/registry.js.map +1 -0
- package/dist/setup.d.ts +10 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +29 -0
- package/dist/setup.js.map +1 -0
- package/nip-ca-server/Dockerfile +27 -0
- package/nip-ca-server/README.md +45 -0
- package/nip-ca-server/db/001_init.sql +25 -0
- package/nip-ca-server/docker-compose.yml +29 -0
- package/nip-ca-server/package.json +23 -0
- package/nip-ca-server/src/ca.ts +155 -0
- package/nip-ca-server/src/db.ts +104 -0
- package/nip-ca-server/src/index.ts +157 -0
- package/nip-ca-server/tsconfig.json +13 -0
- package/package.json +47 -0
- package/src/core/anchor-cache.ts +129 -0
- package/src/core/cache.ts +93 -0
- package/src/core/canonical-json.ts +50 -0
- package/src/core/codec.ts +158 -0
- package/src/core/codecs/index.ts +5 -0
- package/src/core/codecs/ncp-codec.ts +170 -0
- package/src/core/codecs/tier1-json-codec.ts +33 -0
- package/src/core/codecs/tier2-msgpack-codec.ts +30 -0
- package/src/core/crypto-provider.ts +47 -0
- package/src/core/exceptions.ts +57 -0
- package/src/core/frame-header.ts +282 -0
- package/src/core/frame-registry.ts +91 -0
- package/src/core/frames.ts +183 -0
- package/src/core/index.ts +10 -0
- package/src/core/registry.ts +28 -0
- package/src/core/status-codes.ts +46 -0
- package/src/index.ts +10 -0
- package/src/ncp/frames/anchor-frame.ts +87 -0
- package/src/ncp/frames/caps-frame.ts +59 -0
- package/src/ncp/frames/diff-frame.ts +69 -0
- package/src/ncp/frames/error-frame.ts +26 -0
- package/src/ncp/frames/hello-frame.ts +50 -0
- package/src/ncp/frames/stream-frame.ts +35 -0
- package/src/ncp/frames.ts +199 -0
- package/src/ncp/handshake.ts +95 -0
- package/src/ncp/index.ts +12 -0
- package/src/ncp/ncp-error-codes.ts +34 -0
- package/src/ncp/ncp-patch-format.ts +16 -0
- package/src/ncp/registry.ts +14 -0
- package/src/ncp/stream-manager.ts +212 -0
- package/src/ndp/frames.ts +124 -0
- package/src/ndp/index.ts +7 -0
- package/src/ndp/ndp-registry.ts +82 -0
- package/src/ndp/registry.ts +12 -0
- package/src/ndp/validator.ts +64 -0
- package/src/nip/frames.ts +106 -0
- package/src/nip/identity.ts +113 -0
- package/src/nip/index.ts +6 -0
- package/src/nip/registry.ts +12 -0
- package/src/nop/client.ts +103 -0
- package/src/nop/frames.ts +181 -0
- package/src/nop/index.ts +7 -0
- package/src/nop/models.ts +79 -0
- package/src/nop/nop-types.ts +208 -0
- package/src/nop/registry.ts +13 -0
- package/src/nwp/client.ts +114 -0
- package/src/nwp/frames.ts +116 -0
- package/src/nwp/index.ts +6 -0
- package/src/nwp/registry.ts +11 -0
- package/src/setup.ts +32 -0
- package/tests/core/anchor-cache.test.ts +242 -0
- package/tests/core/codec.test.ts +205 -0
- package/tests/core/frame-registry.test.ts +46 -0
- package/tests/core.test.ts +327 -0
- package/tests/ncp/diff-binary-bitset.test.ts +107 -0
- package/tests/ncp/e2e-enc-reject.test.ts +93 -0
- package/tests/ncp/err-error-frame.test.ts +152 -0
- package/tests/ncp/frames.test.ts +359 -0
- package/tests/ncp/framing.test.ts +233 -0
- package/tests/ncp/hello-frame.test.ts +122 -0
- package/tests/ncp/inline-anchor.test.ts +88 -0
- package/tests/ncp/security.test.ts +184 -0
- package/tests/ncp/stream-window.test.ts +167 -0
- package/tests/ncp/stream.test.ts +242 -0
- package/tests/ncp/version-negotiation.test.ts +123 -0
- package/tests/ndp.test.ts +271 -0
- package/tests/nip.test.ts +184 -0
- package/tests/nop.test.ts +344 -0
- package/tests/nwp.test.ts +237 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +20 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
|
|
3
|
+
//
|
|
4
|
+
// NCP Test Cases — StreamFrame + StreamManager
|
|
5
|
+
// Covers: NCP-S-01 to NCP-S-06, NCP-S-12 (UUID format), NCP-S-13 (unknown stream_id)
|
|
6
|
+
// Source: test/ncp_test_cases.md §3.3
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from "vitest";
|
|
9
|
+
import { NcpError } from "../../src/core/frame-header.js";
|
|
10
|
+
import { StreamManager } from "../../src/ncp/stream-manager.js";
|
|
11
|
+
import { validateStreamFrame, type StreamFrame } from "../../src/ncp/frames/stream-frame.js";
|
|
12
|
+
|
|
13
|
+
function chunk(
|
|
14
|
+
streamId: string,
|
|
15
|
+
seq: number,
|
|
16
|
+
data: unknown[],
|
|
17
|
+
opts?: { is_last?: boolean; error_code?: string },
|
|
18
|
+
): StreamFrame {
|
|
19
|
+
return {
|
|
20
|
+
frame: "0x03",
|
|
21
|
+
stream_id: streamId,
|
|
22
|
+
seq,
|
|
23
|
+
is_last: opts?.is_last ?? false,
|
|
24
|
+
data,
|
|
25
|
+
error_code: opts?.error_code,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ===========================================================================
|
|
30
|
+
// NCP-S-01: Sequential Chunks
|
|
31
|
+
// ===========================================================================
|
|
32
|
+
|
|
33
|
+
describe("NCP-S-01: Sequential Chunks", () => {
|
|
34
|
+
// -----------------------------------------------------------------------
|
|
35
|
+
// Spec: §4.3 — seq 0, 1, 2... with is_last=true on final chunk
|
|
36
|
+
// -----------------------------------------------------------------------
|
|
37
|
+
it("reassembles sequential chunks", () => {
|
|
38
|
+
const mgr = new StreamManager();
|
|
39
|
+
|
|
40
|
+
expect(mgr.receive(chunk("s1", 0, ["A", "B"]))).toBe(false);
|
|
41
|
+
expect(mgr.receive(chunk("s1", 1, ["C", "D"]))).toBe(false);
|
|
42
|
+
expect(mgr.receive(chunk("s1", 2, ["E"], { is_last: true }))).toBe(true);
|
|
43
|
+
|
|
44
|
+
const data = mgr.getData("s1");
|
|
45
|
+
expect(data).toEqual(["A", "B", "C", "D", "E"]);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ===========================================================================
|
|
50
|
+
// NCP-S-02: Out of Order Gap
|
|
51
|
+
// ===========================================================================
|
|
52
|
+
|
|
53
|
+
describe("NCP-S-02: Out of Order Gap", () => {
|
|
54
|
+
// -----------------------------------------------------------------------
|
|
55
|
+
// Spec: §4.3 — Sequence numbers MUST be strictly sequential
|
|
56
|
+
// -----------------------------------------------------------------------
|
|
57
|
+
it("rejects sequence gap", () => {
|
|
58
|
+
const mgr = new StreamManager();
|
|
59
|
+
mgr.receive(chunk("s1", 0, ["A"]));
|
|
60
|
+
|
|
61
|
+
expect(() => mgr.receive(chunk("s1", 2, ["C"]))).toThrow(NcpError);
|
|
62
|
+
try {
|
|
63
|
+
mgr.receive(chunk("s1", 2, ["C"]));
|
|
64
|
+
} catch (e) {
|
|
65
|
+
expect((e as NcpError).code).toBe("NCP-STREAM-SEQ-GAP");
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ===========================================================================
|
|
71
|
+
// NCP-S-03: Duplicate Sequence
|
|
72
|
+
// ===========================================================================
|
|
73
|
+
|
|
74
|
+
describe("NCP-S-03: Duplicate Sequence", () => {
|
|
75
|
+
// -----------------------------------------------------------------------
|
|
76
|
+
// Spec: §4.3 — Duplicate seq: ignore or error (both acceptable)
|
|
77
|
+
// Our implementation: ignore (idempotent)
|
|
78
|
+
// -----------------------------------------------------------------------
|
|
79
|
+
it("ignores duplicate sequence number", () => {
|
|
80
|
+
const mgr = new StreamManager();
|
|
81
|
+
mgr.receive(chunk("s1", 0, ["A"]));
|
|
82
|
+
const result = mgr.receive(chunk("s1", 0, ["A-dup"])); // duplicate
|
|
83
|
+
expect(result).toBe(false); // ignored
|
|
84
|
+
|
|
85
|
+
mgr.receive(chunk("s1", 1, ["B"], { is_last: true }));
|
|
86
|
+
const data = mgr.getData("s1");
|
|
87
|
+
expect(data).toEqual(["A", "B"]); // no duplicate data
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ===========================================================================
|
|
92
|
+
// NCP-S-04: Stream ID Conflict
|
|
93
|
+
// ===========================================================================
|
|
94
|
+
|
|
95
|
+
describe("NCP-S-04: Stream ID Conflict", () => {
|
|
96
|
+
// -----------------------------------------------------------------------
|
|
97
|
+
// Spec: §4.3 — New stream MUST NOT reuse an active stream_id.
|
|
98
|
+
// test_cases.md: spec does not define a dedicated code; implementations
|
|
99
|
+
// SHOULD use NPS-CLIENT-CONFLICT until one is assigned.
|
|
100
|
+
// -----------------------------------------------------------------------
|
|
101
|
+
it("rejects reuse of completed stream_id with NPS-CLIENT-CONFLICT", () => {
|
|
102
|
+
const mgr = new StreamManager();
|
|
103
|
+
mgr.receive(chunk("s1", 0, ["A"], { is_last: true }));
|
|
104
|
+
|
|
105
|
+
// Try to reuse s1 — it's completed
|
|
106
|
+
expect(() => mgr.receive(chunk("s1", 0, ["B"]))).toThrow(NcpError);
|
|
107
|
+
try {
|
|
108
|
+
mgr.receive(chunk("s1", 0, ["B"]));
|
|
109
|
+
} catch (e) {
|
|
110
|
+
expect((e as NcpError).code).toBe("NPS-CLIENT-CONFLICT");
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ===========================================================================
|
|
116
|
+
// NCP-S-05: Stream Flooding
|
|
117
|
+
// ===========================================================================
|
|
118
|
+
|
|
119
|
+
describe("NCP-S-05: Stream Flooding", () => {
|
|
120
|
+
// -----------------------------------------------------------------------
|
|
121
|
+
// Spec: §7.3 — Max concurrent streams (default 32)
|
|
122
|
+
// -----------------------------------------------------------------------
|
|
123
|
+
it("rejects opening more than max concurrent streams", () => {
|
|
124
|
+
const mgr = new StreamManager({ maxConcurrent: 3 });
|
|
125
|
+
|
|
126
|
+
mgr.receive(chunk("s1", 0, ["A"]));
|
|
127
|
+
mgr.receive(chunk("s2", 0, ["B"]));
|
|
128
|
+
mgr.receive(chunk("s3", 0, ["C"]));
|
|
129
|
+
|
|
130
|
+
// 4th stream exceeds limit
|
|
131
|
+
expect(() => mgr.receive(chunk("s4", 0, ["D"]))).toThrow(NcpError);
|
|
132
|
+
try {
|
|
133
|
+
mgr.receive(chunk("s4", 0, ["D"]));
|
|
134
|
+
} catch (e) {
|
|
135
|
+
expect((e as NcpError).code).toBe("NCP-STREAM-LIMIT-EXCEEDED");
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ===========================================================================
|
|
141
|
+
// NCP-S-06: Early Termination
|
|
142
|
+
// ===========================================================================
|
|
143
|
+
|
|
144
|
+
describe("NCP-S-06: Early Termination", () => {
|
|
145
|
+
// -----------------------------------------------------------------------
|
|
146
|
+
// Spec: §4.3 — error_code terminates stream, is_last forced true
|
|
147
|
+
// -----------------------------------------------------------------------
|
|
148
|
+
it("terminates stream on error_code", () => {
|
|
149
|
+
const mgr = new StreamManager();
|
|
150
|
+
|
|
151
|
+
mgr.receive(chunk("s1", 0, ["A"]));
|
|
152
|
+
mgr.receive(chunk("s1", 1, ["B"]));
|
|
153
|
+
|
|
154
|
+
const done = mgr.receive(
|
|
155
|
+
chunk("s1", 2, [], { error_code: "NCP-STREAM-SEQ-GAP" }),
|
|
156
|
+
);
|
|
157
|
+
expect(done).toBe(true);
|
|
158
|
+
|
|
159
|
+
// Partial data available
|
|
160
|
+
const data = mgr.getData("s1");
|
|
161
|
+
expect(data).toEqual(["A", "B"]);
|
|
162
|
+
|
|
163
|
+
// Error propagated
|
|
164
|
+
expect(mgr.getError("s1")).toBe("NCP-STREAM-SEQ-GAP");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ===========================================================================
|
|
169
|
+
// NCP-S-12: Invalid stream_id format
|
|
170
|
+
// ===========================================================================
|
|
171
|
+
|
|
172
|
+
describe("NCP-S-12: Invalid stream_id format", () => {
|
|
173
|
+
// -----------------------------------------------------------------------
|
|
174
|
+
// Spec: §4.3 stream_id MUST be UUID v4
|
|
175
|
+
// -----------------------------------------------------------------------
|
|
176
|
+
const validV4 = "550e8400-e29b-41d4-a716-446655440000";
|
|
177
|
+
const invalidExamples = [
|
|
178
|
+
"not-a-uuid",
|
|
179
|
+
"550e8400-e29b-41d4-a716-44665544000", // one char short
|
|
180
|
+
"550e8400-e29b-11d4-a716-446655440000", // v1, not v4
|
|
181
|
+
"550e8400-e29b-41d4-c716-446655440000", // wrong variant nibble
|
|
182
|
+
"", // empty
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
it("accepts a valid UUID v4 stream_id", () => {
|
|
186
|
+
expect(() =>
|
|
187
|
+
validateStreamFrame({
|
|
188
|
+
frame: "0x03",
|
|
189
|
+
stream_id: validV4,
|
|
190
|
+
seq: 0,
|
|
191
|
+
is_last: false,
|
|
192
|
+
data: [],
|
|
193
|
+
}),
|
|
194
|
+
).not.toThrow();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it.each(invalidExamples)(
|
|
198
|
+
"rejects stream_id=%s with NPS-CLIENT-BAD-FRAME",
|
|
199
|
+
(bad) => {
|
|
200
|
+
try {
|
|
201
|
+
validateStreamFrame({
|
|
202
|
+
frame: "0x03",
|
|
203
|
+
stream_id: bad,
|
|
204
|
+
seq: 0,
|
|
205
|
+
is_last: false,
|
|
206
|
+
data: [],
|
|
207
|
+
});
|
|
208
|
+
throw new Error("validateStreamFrame should have thrown");
|
|
209
|
+
} catch (e) {
|
|
210
|
+
expect(e).toBeInstanceOf(NcpError);
|
|
211
|
+
expect((e as NcpError).code).toBe("NPS-CLIENT-BAD-FRAME");
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ===========================================================================
|
|
218
|
+
// NCP-S-13: Unknown stream_id
|
|
219
|
+
// ===========================================================================
|
|
220
|
+
|
|
221
|
+
describe("NCP-S-13: Unknown stream_id", () => {
|
|
222
|
+
// -----------------------------------------------------------------------
|
|
223
|
+
// Spec: §4.3 — a frame for a stream_id that was never opened (seq > 0
|
|
224
|
+
// without a prior seq=0 opener) MUST be rejected with NCP-STREAM-NOT-FOUND.
|
|
225
|
+
// -----------------------------------------------------------------------
|
|
226
|
+
it("rejects seq>0 on a never-opened stream_id", () => {
|
|
227
|
+
const mgr = new StreamManager();
|
|
228
|
+
expect(() => mgr.receive(chunk("never-opened", 5, ["X"]))).toThrow(
|
|
229
|
+
NcpError,
|
|
230
|
+
);
|
|
231
|
+
try {
|
|
232
|
+
mgr.receive(chunk("never-opened", 7, ["Y"]));
|
|
233
|
+
} catch (e) {
|
|
234
|
+
expect((e as NcpError).code).toBe("NCP-STREAM-NOT-FOUND");
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("accepts seq=0 on a previously-unseen stream_id (opener)", () => {
|
|
239
|
+
const mgr = new StreamManager();
|
|
240
|
+
expect(mgr.receive(chunk("new-stream", 0, ["first"]))).toBe(false);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
|
|
3
|
+
//
|
|
4
|
+
// NCP Test Cases — NCP-VN-01 through VN-08: Handshake & Version Negotiation
|
|
5
|
+
// Source: test/ncp_test_cases.md §7
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from "vitest";
|
|
8
|
+
import { negotiateVersion, negotiateEncoding } from "../../src/ncp/handshake.js";
|
|
9
|
+
|
|
10
|
+
describe("NCP-VN: Handshake & Version Negotiation", () => {
|
|
11
|
+
// -----------------------------------------------------------------------
|
|
12
|
+
// NCP-VN-01: Compatible Versions
|
|
13
|
+
// Client: nps_version=0.4, min_version=0.3. Server: nps_version=0.4
|
|
14
|
+
// Expected: session_version=0.4
|
|
15
|
+
// -----------------------------------------------------------------------
|
|
16
|
+
it("NCP-VN-01: compatible versions — session_version = 0.4", () => {
|
|
17
|
+
const result = negotiateVersion(
|
|
18
|
+
{ nps_version: "0.4", min_version: "0.3" },
|
|
19
|
+
{ nps_version: "0.4" },
|
|
20
|
+
);
|
|
21
|
+
expect(result.compatible).toBe(true);
|
|
22
|
+
expect(result.session_version).toBe("0.4");
|
|
23
|
+
expect(result.error_code).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// -----------------------------------------------------------------------
|
|
27
|
+
// NCP-VN-02: Client Newer (downgrade to Server)
|
|
28
|
+
// Client: nps_version=0.5, min_version=0.3. Server: nps_version=0.4
|
|
29
|
+
// Expected: session_version=0.4 (min of both)
|
|
30
|
+
// -----------------------------------------------------------------------
|
|
31
|
+
it("NCP-VN-02: client newer — session_version = 0.4 (server version)", () => {
|
|
32
|
+
const result = negotiateVersion(
|
|
33
|
+
{ nps_version: "0.5", min_version: "0.3" },
|
|
34
|
+
{ nps_version: "0.4" },
|
|
35
|
+
);
|
|
36
|
+
expect(result.compatible).toBe(true);
|
|
37
|
+
expect(result.session_version).toBe("0.4");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// -----------------------------------------------------------------------
|
|
41
|
+
// NCP-VN-03: Client min_version > Server max
|
|
42
|
+
// Client: nps_version=0.5, min_version=0.5. Server: nps_version=0.4
|
|
43
|
+
// Expected: NCP-VERSION-INCOMPATIBLE + connection closed
|
|
44
|
+
// -----------------------------------------------------------------------
|
|
45
|
+
it("NCP-VN-03: client min_version > server version — incompatible", () => {
|
|
46
|
+
const result = negotiateVersion(
|
|
47
|
+
{ nps_version: "0.5", min_version: "0.5" },
|
|
48
|
+
{ nps_version: "0.4" },
|
|
49
|
+
);
|
|
50
|
+
expect(result.compatible).toBe(false);
|
|
51
|
+
expect(result.error_code).toBe("NCP-VERSION-INCOMPATIBLE");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// -----------------------------------------------------------------------
|
|
55
|
+
// NCP-VN-04: Encoding Intersection (msgpack preferred)
|
|
56
|
+
// Client=[msgpack, json], Server=[msgpack, json] → msgpack
|
|
57
|
+
// -----------------------------------------------------------------------
|
|
58
|
+
it("NCP-VN-04: encoding intersection — msgpack preferred over json", () => {
|
|
59
|
+
const result = negotiateEncoding(["msgpack", "json"], ["msgpack", "json"]);
|
|
60
|
+
expect(result.encoding).toBe("msgpack");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// -----------------------------------------------------------------------
|
|
64
|
+
// NCP-VN-05: Encoding Intersection (json only)
|
|
65
|
+
// Client=[json], Server=[msgpack, json] → json
|
|
66
|
+
// -----------------------------------------------------------------------
|
|
67
|
+
it("NCP-VN-05: encoding intersection — json when client only supports json", () => {
|
|
68
|
+
const result = negotiateEncoding(["json"], ["msgpack", "json"]);
|
|
69
|
+
expect(result.encoding).toBe("json");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// -----------------------------------------------------------------------
|
|
73
|
+
// NCP-VN-06: Empty Encoding Intersection
|
|
74
|
+
// Client=[json], Server=[msgpack] → null (no common encoding).
|
|
75
|
+
// Per spec §2.6, this outcome MUST fail the handshake — the session layer
|
|
76
|
+
// treats encoding=null as fatal and the server returns an ErrorFrame.
|
|
77
|
+
// -----------------------------------------------------------------------
|
|
78
|
+
it("NCP-VN-06: empty encoding intersection returns null AND is a fatal handshake outcome", () => {
|
|
79
|
+
const result = negotiateEncoding(["json"], ["msgpack"]);
|
|
80
|
+
expect(result.encoding).toBeNull();
|
|
81
|
+
|
|
82
|
+
// Session-layer contract: null encoding is a fatal handshake error. The
|
|
83
|
+
// session code (not yet implemented) MUST treat this as unrecoverable.
|
|
84
|
+
// We assert the contract here so a future session impl cannot silently
|
|
85
|
+
// downgrade to an undefined/default encoding.
|
|
86
|
+
const handshakeFailsWhenEncodingIsNull = result.encoding === null;
|
|
87
|
+
expect(handshakeFailsWhenEncodingIsNull).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// -----------------------------------------------------------------------
|
|
91
|
+
// NCP-VN-07: Server 5s Timeout
|
|
92
|
+
// This is a transport-layer concern; we validate the expected behaviour
|
|
93
|
+
// is documented by checking the spec reference.
|
|
94
|
+
// -----------------------------------------------------------------------
|
|
95
|
+
it("NCP-VN-07: server timeout is a transport concern — client SHOULD disconnect after 5s", () => {
|
|
96
|
+
// No unit-testable function for this; spec §7 states client SHOULD disconnect.
|
|
97
|
+
// Placeholder: assert the documented timeout value.
|
|
98
|
+
const HELLO_TIMEOUT_MS = 5000;
|
|
99
|
+
expect(HELLO_TIMEOUT_MS).toBe(5000);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// -----------------------------------------------------------------------
|
|
103
|
+
// NCP-VN-08: max_frame_payload Negotiation
|
|
104
|
+
// Client=65535, Server=131072 → session = 65535 (min)
|
|
105
|
+
// -----------------------------------------------------------------------
|
|
106
|
+
it("NCP-VN-08: max_frame_payload = min(client, server)", () => {
|
|
107
|
+
const clientMax = 65535;
|
|
108
|
+
const serverMax = 131072;
|
|
109
|
+
const sessionMax = Math.min(clientMax, serverMax);
|
|
110
|
+
expect(sessionMax).toBe(65535);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Extra: min_version absent — falls back to nps_version
|
|
114
|
+
it("NCP-VN-extra: min_version absent — uses nps_version as effective minimum", () => {
|
|
115
|
+
// Client: nps_version=0.4, no min_version. Server: nps_version=0.4 → compatible
|
|
116
|
+
const result = negotiateVersion(
|
|
117
|
+
{ nps_version: "0.4" },
|
|
118
|
+
{ nps_version: "0.4" },
|
|
119
|
+
);
|
|
120
|
+
expect(result.compatible).toBe(true);
|
|
121
|
+
expect(result.session_version).toBe("0.4");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { AnnounceFrame, ResolveFrame, GraphFrame } from "../src/ndp/frames.js";
|
|
6
|
+
import { InMemoryNdpRegistry } from "../src/ndp/ndp-registry.js";
|
|
7
|
+
import { NdpAnnounceValidator, NdpAnnounceResult } from "../src/ndp/validator.js";
|
|
8
|
+
import { NipIdentity } from "../src/nip/identity.js";
|
|
9
|
+
import { createFullRegistry } from "../src/setup.js";
|
|
10
|
+
import { NpsFrameCodec } from "../src/core/index.js";
|
|
11
|
+
|
|
12
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const NID = "urn:nps:node:example.com:data";
|
|
15
|
+
const ADDRS = [{ host: "example.com", port: 17433, protocol: "nwp" }];
|
|
16
|
+
const CAPS = ["nwp/query", "nwp/stream"];
|
|
17
|
+
|
|
18
|
+
function makeAnnounce(nid = NID, ttl = 300, id?: NipIdentity): AnnounceFrame {
|
|
19
|
+
const ident = id ?? NipIdentity.generate();
|
|
20
|
+
const timestamp = "2026-01-01T00:00:00Z";
|
|
21
|
+
const unsigned = {
|
|
22
|
+
nid, addresses: ADDRS, capabilities: CAPS, ttl, timestamp, node_type: null,
|
|
23
|
+
};
|
|
24
|
+
const sig = ident.sign(unsigned);
|
|
25
|
+
return new AnnounceFrame(nid, ADDRS, CAPS, ttl, timestamp, sig);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── AnnounceFrame round-trip ──────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
describe("AnnounceFrame", () => {
|
|
31
|
+
it("toDict / fromDict roundtrip", () => {
|
|
32
|
+
const f = makeAnnounce();
|
|
33
|
+
const back = AnnounceFrame.fromDict(f.toDict());
|
|
34
|
+
expect(back.nid).toBe(NID);
|
|
35
|
+
expect(back.ttl).toBe(300);
|
|
36
|
+
expect(back.addresses[0]?.port).toBe(17433);
|
|
37
|
+
expect(back.capabilities).toContain("nwp/query");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("unsignedDict omits signature", () => {
|
|
41
|
+
const f = makeAnnounce();
|
|
42
|
+
const d = f.unsignedDict();
|
|
43
|
+
expect(d["signature"]).toBeUndefined();
|
|
44
|
+
expect(d["nid"]).toBe(NID);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("codec roundtrip (MsgPack)", () => {
|
|
48
|
+
const registry = createFullRegistry();
|
|
49
|
+
const codec = new NpsFrameCodec(registry);
|
|
50
|
+
const f = makeAnnounce();
|
|
51
|
+
const back = codec.decode(codec.encode(f)) as AnnounceFrame;
|
|
52
|
+
expect(back).toBeInstanceOf(AnnounceFrame);
|
|
53
|
+
expect(back.nid).toBe(NID);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ── ResolveFrame round-trip ───────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
describe("ResolveFrame", () => {
|
|
60
|
+
it("toDict / fromDict with resolved", () => {
|
|
61
|
+
const f = new ResolveFrame("nwp://example.com/data", "urn:nps:node:a:b", { host: "example.com", port: 17433, ttl: 300 });
|
|
62
|
+
const back = ResolveFrame.fromDict(f.toDict());
|
|
63
|
+
expect(back.target).toBe("nwp://example.com/data");
|
|
64
|
+
expect(back.requesterNid).toBe("urn:nps:node:a:b");
|
|
65
|
+
expect(back.resolved?.port).toBe(17433);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("toDict / fromDict without optional fields", () => {
|
|
69
|
+
const f = new ResolveFrame("nwp://example.com/data");
|
|
70
|
+
const back = ResolveFrame.fromDict(f.toDict());
|
|
71
|
+
expect(back.requesterNid).toBeUndefined();
|
|
72
|
+
expect(back.resolved).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── GraphFrame round-trip ─────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
describe("GraphFrame", () => {
|
|
79
|
+
it("toDict / fromDict with nodes", () => {
|
|
80
|
+
const nodes = [{ nid: NID, addresses: ADDRS, capabilities: CAPS }];
|
|
81
|
+
const f = new GraphFrame(1, true, nodes);
|
|
82
|
+
const back = GraphFrame.fromDict(f.toDict());
|
|
83
|
+
expect(back.seq).toBe(1);
|
|
84
|
+
expect(back.initialSync).toBe(true);
|
|
85
|
+
expect(back.nodes?.[0]?.nid).toBe(NID);
|
|
86
|
+
expect(back.patch).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ── InMemoryNdpRegistry ───────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
describe("InMemoryNdpRegistry", () => {
|
|
93
|
+
it("announce + getByNid", () => {
|
|
94
|
+
const reg = new InMemoryNdpRegistry();
|
|
95
|
+
const f = makeAnnounce();
|
|
96
|
+
reg.announce(f);
|
|
97
|
+
expect(reg.getByNid(NID)).toBe(f);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("getByNid returns undefined for unknown NID", () => {
|
|
101
|
+
const reg = new InMemoryNdpRegistry();
|
|
102
|
+
expect(reg.getByNid("urn:nps:node:unknown:x")).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("announce with ttl=0 removes entry", () => {
|
|
106
|
+
const reg = new InMemoryNdpRegistry();
|
|
107
|
+
reg.announce(makeAnnounce(NID, 300));
|
|
108
|
+
expect(reg.getByNid(NID)).toBeDefined();
|
|
109
|
+
reg.announce(makeAnnounce(NID, 0));
|
|
110
|
+
expect(reg.getByNid(NID)).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("TTL expiry — getByNid returns undefined after expiry", () => {
|
|
114
|
+
const reg = new InMemoryNdpRegistry();
|
|
115
|
+
let now = 0;
|
|
116
|
+
reg.clock = () => now;
|
|
117
|
+
reg.announce(makeAnnounce(NID, 10));
|
|
118
|
+
now = 11_000;
|
|
119
|
+
expect(reg.getByNid(NID)).toBeUndefined();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("resolve returns host/port for matching target", () => {
|
|
123
|
+
const reg = new InMemoryNdpRegistry();
|
|
124
|
+
reg.announce(makeAnnounce());
|
|
125
|
+
const r = reg.resolve("nwp://example.com/data/sub");
|
|
126
|
+
expect(r).toBeDefined();
|
|
127
|
+
expect(r?.host).toBe("example.com");
|
|
128
|
+
expect(r?.port).toBe(17433);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("resolve returns undefined for non-matching target", () => {
|
|
132
|
+
const reg = new InMemoryNdpRegistry();
|
|
133
|
+
reg.announce(makeAnnounce());
|
|
134
|
+
expect(reg.resolve("nwp://other.com/data")).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("getAll returns active entries", () => {
|
|
138
|
+
const reg = new InMemoryNdpRegistry();
|
|
139
|
+
let now = 0;
|
|
140
|
+
reg.clock = () => now;
|
|
141
|
+
reg.announce(makeAnnounce("urn:nps:node:a.com:x", 100));
|
|
142
|
+
reg.announce(makeAnnounce("urn:nps:node:b.com:y", 1));
|
|
143
|
+
now = 2_000; // b expired
|
|
144
|
+
const all = reg.getAll();
|
|
145
|
+
expect(all).toHaveLength(1);
|
|
146
|
+
expect(all[0]?.nid).toBe("urn:nps:node:a.com:x");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("resolve skips expired entries", () => {
|
|
150
|
+
const reg = new InMemoryNdpRegistry();
|
|
151
|
+
let now = 0;
|
|
152
|
+
reg.clock = () => now;
|
|
153
|
+
reg.announce(makeAnnounce(NID, 5));
|
|
154
|
+
now = 10_000;
|
|
155
|
+
expect(reg.resolve("nwp://example.com/data")).toBeUndefined();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ── nwpTargetMatchesNid ───────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
describe("InMemoryNdpRegistry.nwpTargetMatchesNid", () => {
|
|
162
|
+
const match = InMemoryNdpRegistry.nwpTargetMatchesNid;
|
|
163
|
+
|
|
164
|
+
it("exact match", () => {
|
|
165
|
+
expect(match("urn:nps:node:example.com:data", "nwp://example.com/data")).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("sub-path match", () => {
|
|
169
|
+
expect(match("urn:nps:node:example.com:data", "nwp://example.com/data/sub")).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("different authority does not match", () => {
|
|
173
|
+
expect(match("urn:nps:node:other.com:data", "nwp://example.com/data")).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("sibling path does not match", () => {
|
|
177
|
+
expect(match("urn:nps:node:example.com:data", "nwp://example.com/dataset")).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("invalid NID format returns false", () => {
|
|
181
|
+
expect(match("invalid-nid", "nwp://example.com/data")).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("non-nwp:// target returns false", () => {
|
|
185
|
+
expect(match("urn:nps:node:example.com:data", "http://example.com/data")).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("target without path slash returns false", () => {
|
|
189
|
+
expect(match("urn:nps:node:example.com:data", "nwp://example.com")).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ── NdpAnnounceResult ─────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
describe("NdpAnnounceResult", () => {
|
|
196
|
+
it("ok() returns isValid=true", () => {
|
|
197
|
+
const r = NdpAnnounceResult.ok();
|
|
198
|
+
expect(r.isValid).toBe(true);
|
|
199
|
+
expect(r.errorCode).toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("fail() returns isValid=false with code + message", () => {
|
|
203
|
+
const r = NdpAnnounceResult.fail("NDP-ERR", "bad sig");
|
|
204
|
+
expect(r.isValid).toBe(false);
|
|
205
|
+
expect(r.errorCode).toBe("NDP-ERR");
|
|
206
|
+
expect(r.message).toBe("bad sig");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ── NdpAnnounceValidator ──────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
describe("NdpAnnounceValidator", () => {
|
|
213
|
+
it("fails when no key registered", () => {
|
|
214
|
+
const v = new NdpAnnounceValidator();
|
|
215
|
+
const r = v.validate(makeAnnounce());
|
|
216
|
+
expect(r.isValid).toBe(false);
|
|
217
|
+
expect(r.errorCode).toBe("NDP-ANNOUNCE-NID-MISMATCH");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("validates a correctly signed frame", () => {
|
|
221
|
+
const ident = NipIdentity.generate();
|
|
222
|
+
const v = new NdpAnnounceValidator();
|
|
223
|
+
v.registerPublicKey(NID, ident.pubKeyString);
|
|
224
|
+
const f = makeAnnounce(NID, 300, ident);
|
|
225
|
+
expect(v.validate(f).isValid).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("rejects tampered frame (wrong signature)", () => {
|
|
229
|
+
const ident = NipIdentity.generate();
|
|
230
|
+
const v = new NdpAnnounceValidator();
|
|
231
|
+
v.registerPublicKey(NID, ident.pubKeyString);
|
|
232
|
+
// Build frame signed by a different key
|
|
233
|
+
const other = NipIdentity.generate();
|
|
234
|
+
const f = makeAnnounce(NID, 300, other);
|
|
235
|
+
expect(v.validate(f).isValid).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("rejects signature with wrong prefix", () => {
|
|
239
|
+
const ident = NipIdentity.generate();
|
|
240
|
+
const v = new NdpAnnounceValidator();
|
|
241
|
+
v.registerPublicKey(NID, ident.pubKeyString);
|
|
242
|
+
const f = new AnnounceFrame(NID, ADDRS, CAPS, 300, "2026-01-01T00:00:00Z", "rsa:invalid");
|
|
243
|
+
const r = v.validate(f);
|
|
244
|
+
expect(r.isValid).toBe(false);
|
|
245
|
+
expect(r.errorCode).toBe("NDP-ANNOUNCE-SIG-INVALID");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("rejects corrupted base64 signature", () => {
|
|
249
|
+
const ident = NipIdentity.generate();
|
|
250
|
+
const v = new NdpAnnounceValidator();
|
|
251
|
+
v.registerPublicKey(NID, ident.pubKeyString);
|
|
252
|
+
const f = new AnnounceFrame(NID, ADDRS, CAPS, 300, "2026-01-01T00:00:00Z", "ed25519:!!!garbage!!!");
|
|
253
|
+
const r = v.validate(f);
|
|
254
|
+
expect(r.isValid).toBe(false);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("removePublicKey removes registration", () => {
|
|
258
|
+
const ident = NipIdentity.generate();
|
|
259
|
+
const v = new NdpAnnounceValidator();
|
|
260
|
+
v.registerPublicKey(NID, ident.pubKeyString);
|
|
261
|
+
v.removePublicKey(NID);
|
|
262
|
+
expect(v.knownPublicKeys.has(NID)).toBe(false);
|
|
263
|
+
expect(v.validate(makeAnnounce(NID, 300, ident)).isValid).toBe(false);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("knownPublicKeys is readonly view", () => {
|
|
267
|
+
const v = new NdpAnnounceValidator();
|
|
268
|
+
v.registerPublicKey("urn:nps:node:a:1", "ed25519:aabb");
|
|
269
|
+
expect(v.knownPublicKeys.size).toBe(1);
|
|
270
|
+
});
|
|
271
|
+
});
|