@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.
Files changed (311) hide show
  1. package/CONTRIBUTING.md +33 -0
  2. package/LICENSE +170 -0
  3. package/NOTICE +7 -0
  4. package/README.md +153 -0
  5. package/dist/codec-CmHeovTV.d.cts +120 -0
  6. package/dist/codec-CmHeovTV.d.ts +120 -0
  7. package/dist/core/anchor-cache.d.ts +42 -0
  8. package/dist/core/anchor-cache.d.ts.map +1 -0
  9. package/dist/core/anchor-cache.js +104 -0
  10. package/dist/core/anchor-cache.js.map +1 -0
  11. package/dist/core/cache.d.ts +14 -0
  12. package/dist/core/cache.d.ts.map +1 -0
  13. package/dist/core/cache.js +80 -0
  14. package/dist/core/cache.js.map +1 -0
  15. package/dist/core/canonical-json.d.ts +12 -0
  16. package/dist/core/canonical-json.d.ts.map +1 -0
  17. package/dist/core/canonical-json.js +44 -0
  18. package/dist/core/canonical-json.js.map +1 -0
  19. package/dist/core/codec.d.ts +32 -0
  20. package/dist/core/codec.d.ts.map +1 -0
  21. package/dist/core/codec.js +119 -0
  22. package/dist/core/codec.js.map +1 -0
  23. package/dist/core/codecs/index.d.ts +4 -0
  24. package/dist/core/codecs/index.d.ts.map +1 -0
  25. package/dist/core/codecs/index.js +6 -0
  26. package/dist/core/codecs/index.js.map +1 -0
  27. package/dist/core/codecs/ncp-codec.d.ts +39 -0
  28. package/dist/core/codecs/ncp-codec.d.ts.map +1 -0
  29. package/dist/core/codecs/ncp-codec.js +93 -0
  30. package/dist/core/codecs/ncp-codec.js.map +1 -0
  31. package/dist/core/codecs/tier1-json-codec.d.ts +10 -0
  32. package/dist/core/codecs/tier1-json-codec.d.ts.map +1 -0
  33. package/dist/core/codecs/tier1-json-codec.js +28 -0
  34. package/dist/core/codecs/tier1-json-codec.js.map +1 -0
  35. package/dist/core/codecs/tier2-msgpack-codec.d.ts +10 -0
  36. package/dist/core/codecs/tier2-msgpack-codec.d.ts.map +1 -0
  37. package/dist/core/codecs/tier2-msgpack-codec.js +26 -0
  38. package/dist/core/codecs/tier2-msgpack-codec.js.map +1 -0
  39. package/dist/core/crypto-provider.d.ts +31 -0
  40. package/dist/core/crypto-provider.d.ts.map +1 -0
  41. package/dist/core/crypto-provider.js +10 -0
  42. package/dist/core/crypto-provider.js.map +1 -0
  43. package/dist/core/exceptions.d.ts +27 -0
  44. package/dist/core/exceptions.d.ts.map +1 -0
  45. package/dist/core/exceptions.js +52 -0
  46. package/dist/core/exceptions.js.map +1 -0
  47. package/dist/core/frame-header.d.ts +87 -0
  48. package/dist/core/frame-header.d.ts.map +1 -0
  49. package/dist/core/frame-header.js +185 -0
  50. package/dist/core/frame-header.js.map +1 -0
  51. package/dist/core/frame-registry.d.ts +35 -0
  52. package/dist/core/frame-registry.d.ts.map +1 -0
  53. package/dist/core/frame-registry.js +63 -0
  54. package/dist/core/frame-registry.js.map +1 -0
  55. package/dist/core/frames.d.ts +80 -0
  56. package/dist/core/frames.d.ts.map +1 -0
  57. package/dist/core/frames.js +153 -0
  58. package/dist/core/frames.js.map +1 -0
  59. package/dist/core/index.cjs +371 -0
  60. package/dist/core/index.cjs.map +1 -0
  61. package/dist/core/index.d.cts +41 -0
  62. package/dist/core/index.d.ts +9 -0
  63. package/dist/core/index.d.ts.map +1 -0
  64. package/dist/core/index.js +10 -0
  65. package/dist/core/index.js.map +1 -0
  66. package/dist/core/registry.d.ts +11 -0
  67. package/dist/core/registry.d.ts.map +1 -0
  68. package/dist/core/registry.js +17 -0
  69. package/dist/core/registry.js.map +1 -0
  70. package/dist/core/status-codes.d.ts +28 -0
  71. package/dist/core/status-codes.d.ts.map +1 -0
  72. package/dist/core/status-codes.js +38 -0
  73. package/dist/core/status-codes.js.map +1 -0
  74. package/dist/frames-B3qLdl_g.d.cts +77 -0
  75. package/dist/frames-Ff7-ZPUl.d.ts +77 -0
  76. package/dist/index.cjs +1556 -0
  77. package/dist/index.cjs.map +1 -0
  78. package/dist/index.d.cts +21 -0
  79. package/dist/index.d.ts +2 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +10 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/ncp/frames/anchor-frame.d.ts +29 -0
  84. package/dist/ncp/frames/anchor-frame.d.ts.map +1 -0
  85. package/dist/ncp/frames/anchor-frame.js +54 -0
  86. package/dist/ncp/frames/anchor-frame.js.map +1 -0
  87. package/dist/ncp/frames/caps-frame.d.ts +29 -0
  88. package/dist/ncp/frames/caps-frame.d.ts.map +1 -0
  89. package/dist/ncp/frames/caps-frame.js +29 -0
  90. package/dist/ncp/frames/caps-frame.js.map +1 -0
  91. package/dist/ncp/frames/diff-frame.d.ts +32 -0
  92. package/dist/ncp/frames/diff-frame.d.ts.map +1 -0
  93. package/dist/ncp/frames/diff-frame.js +37 -0
  94. package/dist/ncp/frames/diff-frame.js.map +1 -0
  95. package/dist/ncp/frames/error-frame.d.ts +16 -0
  96. package/dist/ncp/frames/error-frame.d.ts.map +1 -0
  97. package/dist/ncp/frames/error-frame.js +13 -0
  98. package/dist/ncp/frames/error-frame.js.map +1 -0
  99. package/dist/ncp/frames/hello-frame.d.ts +21 -0
  100. package/dist/ncp/frames/hello-frame.d.ts.map +1 -0
  101. package/dist/ncp/frames/hello-frame.js +25 -0
  102. package/dist/ncp/frames/hello-frame.js.map +1 -0
  103. package/dist/ncp/frames/stream-frame.d.ts +16 -0
  104. package/dist/ncp/frames/stream-frame.d.ts.map +1 -0
  105. package/dist/ncp/frames/stream-frame.js +18 -0
  106. package/dist/ncp/frames/stream-frame.js.map +1 -0
  107. package/dist/ncp/frames.d.ts +76 -0
  108. package/dist/ncp/frames.d.ts.map +1 -0
  109. package/dist/ncp/frames.js +147 -0
  110. package/dist/ncp/frames.js.map +1 -0
  111. package/dist/ncp/handshake.d.ts +30 -0
  112. package/dist/ncp/handshake.d.ts.map +1 -0
  113. package/dist/ncp/handshake.js +80 -0
  114. package/dist/ncp/handshake.js.map +1 -0
  115. package/dist/ncp/index.cjs +188 -0
  116. package/dist/ncp/index.cjs.map +1 -0
  117. package/dist/ncp/index.d.cts +6 -0
  118. package/dist/ncp/index.d.ts +11 -0
  119. package/dist/ncp/index.d.ts.map +1 -0
  120. package/dist/ncp/index.js +13 -0
  121. package/dist/ncp/index.js.map +1 -0
  122. package/dist/ncp/ncp-error-codes.d.ts +22 -0
  123. package/dist/ncp/ncp-error-codes.d.ts.map +1 -0
  124. package/dist/ncp/ncp-error-codes.js +32 -0
  125. package/dist/ncp/ncp-error-codes.js.map +1 -0
  126. package/dist/ncp/ncp-patch-format.d.ts +7 -0
  127. package/dist/ncp/ncp-patch-format.d.ts.map +1 -0
  128. package/dist/ncp/ncp-patch-format.js +13 -0
  129. package/dist/ncp/ncp-patch-format.js.map +1 -0
  130. package/dist/ncp/registry.d.ts +3 -0
  131. package/dist/ncp/registry.d.ts.map +1 -0
  132. package/dist/ncp/registry.js +12 -0
  133. package/dist/ncp/registry.js.map +1 -0
  134. package/dist/ncp/stream-manager.d.ts +57 -0
  135. package/dist/ncp/stream-manager.d.ts.map +1 -0
  136. package/dist/ncp/stream-manager.js +163 -0
  137. package/dist/ncp/stream-manager.js.map +1 -0
  138. package/dist/ndp/frames.d.ts +56 -0
  139. package/dist/ndp/frames.d.ts.map +1 -0
  140. package/dist/ndp/frames.js +87 -0
  141. package/dist/ndp/frames.js.map +1 -0
  142. package/dist/ndp/index.cjs +252 -0
  143. package/dist/ndp/index.cjs.map +1 -0
  144. package/dist/ndp/index.d.cts +86 -0
  145. package/dist/ndp/index.d.ts +5 -0
  146. package/dist/ndp/index.d.ts.map +1 -0
  147. package/dist/ndp/index.js +7 -0
  148. package/dist/ndp/index.js.map +1 -0
  149. package/dist/ndp/ndp-registry.d.ts +11 -0
  150. package/dist/ndp/ndp-registry.d.ts.map +1 -0
  151. package/dist/ndp/ndp-registry.js +79 -0
  152. package/dist/ndp/ndp-registry.js.map +1 -0
  153. package/dist/ndp/registry.d.ts +3 -0
  154. package/dist/ndp/registry.d.ts.map +1 -0
  155. package/dist/ndp/registry.js +10 -0
  156. package/dist/ndp/registry.js.map +1 -0
  157. package/dist/ndp/validator.d.ts +18 -0
  158. package/dist/ndp/validator.d.ts.map +1 -0
  159. package/dist/ndp/validator.js +48 -0
  160. package/dist/ndp/validator.js.map +1 -0
  161. package/dist/nip/frames.d.ts +44 -0
  162. package/dist/nip/frames.d.ts.map +1 -0
  163. package/dist/nip/frames.js +81 -0
  164. package/dist/nip/frames.js.map +1 -0
  165. package/dist/nip/identity.d.ts +18 -0
  166. package/dist/nip/identity.d.ts.map +1 -0
  167. package/dist/nip/identity.js +94 -0
  168. package/dist/nip/identity.js.map +1 -0
  169. package/dist/nip/index.cjs +214 -0
  170. package/dist/nip/index.cjs.map +1 -0
  171. package/dist/nip/index.d.cts +65 -0
  172. package/dist/nip/index.d.ts +4 -0
  173. package/dist/nip/index.d.ts.map +1 -0
  174. package/dist/nip/index.js +6 -0
  175. package/dist/nip/index.js.map +1 -0
  176. package/dist/nip/registry.d.ts +3 -0
  177. package/dist/nip/registry.d.ts.map +1 -0
  178. package/dist/nip/registry.js +10 -0
  179. package/dist/nip/registry.js.map +1 -0
  180. package/dist/nop/client.d.ts +34 -0
  181. package/dist/nop/client.d.ts.map +1 -0
  182. package/dist/nop/client.js +90 -0
  183. package/dist/nop/client.js.map +1 -0
  184. package/dist/nop/frames.d.ts +65 -0
  185. package/dist/nop/frames.d.ts.map +1 -0
  186. package/dist/nop/frames.js +148 -0
  187. package/dist/nop/frames.js.map +1 -0
  188. package/dist/nop/index.cjs +762 -0
  189. package/dist/nop/index.cjs.map +1 -0
  190. package/dist/nop/index.d.cts +155 -0
  191. package/dist/nop/index.d.ts +5 -0
  192. package/dist/nop/index.d.ts.map +1 -0
  193. package/dist/nop/index.js +7 -0
  194. package/dist/nop/index.js.map +1 -0
  195. package/dist/nop/models.d.ts +58 -0
  196. package/dist/nop/models.d.ts.map +1 -0
  197. package/dist/nop/models.js +50 -0
  198. package/dist/nop/models.js.map +1 -0
  199. package/dist/nop/nop-types.d.ts +136 -0
  200. package/dist/nop/nop-types.d.ts.map +1 -0
  201. package/dist/nop/nop-types.js +44 -0
  202. package/dist/nop/nop-types.js.map +1 -0
  203. package/dist/nop/registry.d.ts +3 -0
  204. package/dist/nop/registry.d.ts.map +1 -0
  205. package/dist/nop/registry.js +11 -0
  206. package/dist/nop/registry.js.map +1 -0
  207. package/dist/nwp/client.d.ts +22 -0
  208. package/dist/nwp/client.d.ts.map +1 -0
  209. package/dist/nwp/client.js +101 -0
  210. package/dist/nwp/client.js.map +1 -0
  211. package/dist/nwp/frames.d.ts +46 -0
  212. package/dist/nwp/frames.d.ts.map +1 -0
  213. package/dist/nwp/frames.js +81 -0
  214. package/dist/nwp/frames.js.map +1 -0
  215. package/dist/nwp/index.cjs +658 -0
  216. package/dist/nwp/index.cjs.map +1 -0
  217. package/dist/nwp/index.d.cts +65 -0
  218. package/dist/nwp/index.d.ts +4 -0
  219. package/dist/nwp/index.d.ts.map +1 -0
  220. package/dist/nwp/index.js +6 -0
  221. package/dist/nwp/index.js.map +1 -0
  222. package/dist/nwp/registry.d.ts +3 -0
  223. package/dist/nwp/registry.d.ts.map +1 -0
  224. package/dist/nwp/registry.js +9 -0
  225. package/dist/nwp/registry.js.map +1 -0
  226. package/dist/setup.d.ts +10 -0
  227. package/dist/setup.d.ts.map +1 -0
  228. package/dist/setup.js +29 -0
  229. package/dist/setup.js.map +1 -0
  230. package/nip-ca-server/Dockerfile +27 -0
  231. package/nip-ca-server/README.md +45 -0
  232. package/nip-ca-server/db/001_init.sql +25 -0
  233. package/nip-ca-server/docker-compose.yml +29 -0
  234. package/nip-ca-server/package.json +23 -0
  235. package/nip-ca-server/src/ca.ts +155 -0
  236. package/nip-ca-server/src/db.ts +104 -0
  237. package/nip-ca-server/src/index.ts +157 -0
  238. package/nip-ca-server/tsconfig.json +13 -0
  239. package/package.json +47 -0
  240. package/src/core/anchor-cache.ts +129 -0
  241. package/src/core/cache.ts +93 -0
  242. package/src/core/canonical-json.ts +50 -0
  243. package/src/core/codec.ts +158 -0
  244. package/src/core/codecs/index.ts +5 -0
  245. package/src/core/codecs/ncp-codec.ts +170 -0
  246. package/src/core/codecs/tier1-json-codec.ts +33 -0
  247. package/src/core/codecs/tier2-msgpack-codec.ts +30 -0
  248. package/src/core/crypto-provider.ts +47 -0
  249. package/src/core/exceptions.ts +57 -0
  250. package/src/core/frame-header.ts +282 -0
  251. package/src/core/frame-registry.ts +91 -0
  252. package/src/core/frames.ts +183 -0
  253. package/src/core/index.ts +10 -0
  254. package/src/core/registry.ts +28 -0
  255. package/src/core/status-codes.ts +46 -0
  256. package/src/index.ts +10 -0
  257. package/src/ncp/frames/anchor-frame.ts +87 -0
  258. package/src/ncp/frames/caps-frame.ts +59 -0
  259. package/src/ncp/frames/diff-frame.ts +69 -0
  260. package/src/ncp/frames/error-frame.ts +26 -0
  261. package/src/ncp/frames/hello-frame.ts +50 -0
  262. package/src/ncp/frames/stream-frame.ts +35 -0
  263. package/src/ncp/frames.ts +199 -0
  264. package/src/ncp/handshake.ts +95 -0
  265. package/src/ncp/index.ts +12 -0
  266. package/src/ncp/ncp-error-codes.ts +34 -0
  267. package/src/ncp/ncp-patch-format.ts +16 -0
  268. package/src/ncp/registry.ts +14 -0
  269. package/src/ncp/stream-manager.ts +212 -0
  270. package/src/ndp/frames.ts +124 -0
  271. package/src/ndp/index.ts +7 -0
  272. package/src/ndp/ndp-registry.ts +82 -0
  273. package/src/ndp/registry.ts +12 -0
  274. package/src/ndp/validator.ts +64 -0
  275. package/src/nip/frames.ts +106 -0
  276. package/src/nip/identity.ts +113 -0
  277. package/src/nip/index.ts +6 -0
  278. package/src/nip/registry.ts +12 -0
  279. package/src/nop/client.ts +103 -0
  280. package/src/nop/frames.ts +181 -0
  281. package/src/nop/index.ts +7 -0
  282. package/src/nop/models.ts +79 -0
  283. package/src/nop/nop-types.ts +208 -0
  284. package/src/nop/registry.ts +13 -0
  285. package/src/nwp/client.ts +114 -0
  286. package/src/nwp/frames.ts +116 -0
  287. package/src/nwp/index.ts +6 -0
  288. package/src/nwp/registry.ts +11 -0
  289. package/src/setup.ts +32 -0
  290. package/tests/core/anchor-cache.test.ts +242 -0
  291. package/tests/core/codec.test.ts +205 -0
  292. package/tests/core/frame-registry.test.ts +46 -0
  293. package/tests/core.test.ts +327 -0
  294. package/tests/ncp/diff-binary-bitset.test.ts +107 -0
  295. package/tests/ncp/e2e-enc-reject.test.ts +93 -0
  296. package/tests/ncp/err-error-frame.test.ts +152 -0
  297. package/tests/ncp/frames.test.ts +359 -0
  298. package/tests/ncp/framing.test.ts +233 -0
  299. package/tests/ncp/hello-frame.test.ts +122 -0
  300. package/tests/ncp/inline-anchor.test.ts +88 -0
  301. package/tests/ncp/security.test.ts +184 -0
  302. package/tests/ncp/stream-window.test.ts +167 -0
  303. package/tests/ncp/stream.test.ts +242 -0
  304. package/tests/ncp/version-negotiation.test.ts +123 -0
  305. package/tests/ndp.test.ts +271 -0
  306. package/tests/nip.test.ts +184 -0
  307. package/tests/nop.test.ts +344 -0
  308. package/tests/nwp.test.ts +237 -0
  309. package/tsconfig.json +20 -0
  310. package/tsup.config.ts +20 -0
  311. package/vitest.config.ts +10 -0
@@ -0,0 +1,122 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
3
+ //
4
+ // NCP Test Cases — NCP-H-01 through H-06: HelloFrame (0x06)
5
+ // Source: test/ncp_test_cases.md §3.5
6
+
7
+ import { describe, it, expect } from "vitest";
8
+ import { NcpError } from "../../src/core/frame-header.js";
9
+ import { NCP_ERROR_CODES } from "../../src/ncp/ncp-error-codes.js";
10
+ import { validateHelloFrame, type HelloFrame } from "../../src/ncp/frames/hello-frame.js";
11
+
12
+ function makeValidHello(): HelloFrame {
13
+ return {
14
+ frame: "0x06",
15
+ nps_version: "0.4",
16
+ supported_encodings: ["msgpack", "json"],
17
+ supported_protocols: ["ncp", "nwp"],
18
+ };
19
+ }
20
+
21
+ describe("NCP-H: HelloFrame (0x06)", () => {
22
+ // -----------------------------------------------------------------------
23
+ // NCP-H-01: First frame after TCP connect
24
+ // Spec: §3.5 — Client sends HelloFrame as very first frame
25
+ // -----------------------------------------------------------------------
26
+ it("NCP-H-01: accepts a valid HelloFrame with all required fields", () => {
27
+ const frame = makeValidHello();
28
+ expect(() => validateHelloFrame(frame)).not.toThrow();
29
+ });
30
+
31
+ // -----------------------------------------------------------------------
32
+ // NCP-H-02: HelloFrame Not First (protocol enforcement — out of scope for
33
+ // unit test; validateHelloFrame validates structure only)
34
+ // -----------------------------------------------------------------------
35
+ it("NCP-H-02: valid HelloFrame structure passes validation", () => {
36
+ // Connection-ordering enforcement is a session-layer concern.
37
+ // validateHelloFrame validates fields only.
38
+ const frame = makeValidHello();
39
+ expect(() => validateHelloFrame(frame)).not.toThrow();
40
+ });
41
+
42
+ // -----------------------------------------------------------------------
43
+ // NCP-H-03: HelloFrame Missing Required Fields
44
+ // Spec: §3.5 — nps_version, supported_encodings, supported_protocols required
45
+ // -----------------------------------------------------------------------
46
+ it("NCP-H-03: rejects HelloFrame missing nps_version", () => {
47
+ const frame = { ...makeValidHello(), nps_version: "" } as HelloFrame;
48
+ expect(() => validateHelloFrame(frame)).toThrow(NcpError);
49
+ try {
50
+ validateHelloFrame(frame);
51
+ } catch (e) {
52
+ expect((e as NcpError).code).toBe("NPS-CLIENT-BAD-FRAME");
53
+ }
54
+ });
55
+
56
+ it("NCP-H-03: rejects HelloFrame missing supported_encodings", () => {
57
+ const frame = { ...makeValidHello(), supported_encodings: [] } as HelloFrame;
58
+ expect(() => validateHelloFrame(frame)).toThrow(NcpError);
59
+ try {
60
+ validateHelloFrame(frame);
61
+ } catch (e) {
62
+ expect((e as NcpError).code).toBe("NPS-CLIENT-BAD-FRAME");
63
+ }
64
+ });
65
+
66
+ it("NCP-H-03: rejects HelloFrame missing supported_protocols", () => {
67
+ const frame = { ...makeValidHello(), supported_protocols: [] } as HelloFrame;
68
+ expect(() => validateHelloFrame(frame)).toThrow(NcpError);
69
+ try {
70
+ validateHelloFrame(frame);
71
+ } catch (e) {
72
+ expect((e as NcpError).code).toBe("NPS-CLIENT-BAD-FRAME");
73
+ }
74
+ });
75
+
76
+ // -----------------------------------------------------------------------
77
+ // NCP-H-04: HelloFrame with ENC=1
78
+ // Spec: §3.5 — ENC=1 on HelloFrame before negotiation → NCP-ENC-NOT-NEGOTIATED
79
+ // This is checked at the session layer (frame flags), not by validateHelloFrame.
80
+ // We verify the expected error code is a known constant.
81
+ // -----------------------------------------------------------------------
82
+ it("NCP-H-04: NCP-ENC-NOT-NEGOTIATED is the named constant for pre-negotiation ENC=1", () => {
83
+ // Session-layer enforcement: if ENC=1 flag is set on HelloFrame,
84
+ // the receiver MUST return NCP-ENC-NOT-NEGOTIATED. We assert against the
85
+ // canonical constant rather than a raw string, and the negotiation path
86
+ // itself is exercised end-to-end in ncp-e2e-enc-reject.test.ts.
87
+ expect(NCP_ERROR_CODES.NCP_ENC_NOT_NEGOTIATED).toBe(
88
+ "NCP-ENC-NOT-NEGOTIATED",
89
+ );
90
+ // Sanity: the constant is a member of the NcpErrorCode union, which is
91
+ // the type-level promise that downstream code can rely on.
92
+ const code: (typeof NCP_ERROR_CODES)[keyof typeof NCP_ERROR_CODES] =
93
+ NCP_ERROR_CODES.NCP_ENC_NOT_NEGOTIATED;
94
+ expect(code).toBeDefined();
95
+ });
96
+
97
+ // -----------------------------------------------------------------------
98
+ // NCP-H-05: HelloFrame encoded Tier-2
99
+ // Spec: §3.5 — Tier-2 MsgPack is allowed during handshake
100
+ // -----------------------------------------------------------------------
101
+ it("NCP-H-05: validates HelloFrame regardless of encoding tier (structure only)", () => {
102
+ // Tier selection is a codec concern; validateHelloFrame validates fields.
103
+ const frame = makeValidHello();
104
+ expect(() => validateHelloFrame(frame)).not.toThrow();
105
+ });
106
+
107
+ // -----------------------------------------------------------------------
108
+ // NCP-H-06: min_version defaults to nps_version
109
+ // Spec: §3.5 — If min_version omitted, server treats as equal to nps_version
110
+ // -----------------------------------------------------------------------
111
+ it("NCP-H-06: accepts HelloFrame without min_version (optional field)", () => {
112
+ const frame = makeValidHello();
113
+ expect(frame.min_version).toBeUndefined();
114
+ expect(() => validateHelloFrame(frame)).not.toThrow();
115
+ });
116
+
117
+ it("NCP-H-06: accepts HelloFrame with explicit min_version", () => {
118
+ const frame = { ...makeValidHello(), min_version: "0.3" };
119
+ expect(() => validateHelloFrame(frame)).not.toThrow();
120
+ expect(frame.min_version).toBe("0.3");
121
+ });
122
+ });
@@ -0,0 +1,88 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
3
+ //
4
+ // NCP Test Cases — NCP-C-05, C-06, C-07: CapsFrame inline_anchor
5
+ // Source: test/ncp_test_cases.md §3.4
6
+
7
+ import { describe, it, expect } from "vitest";
8
+ import { NcpError } from "../../src/core/frame-header.js";
9
+ import { computeAnchorId, type FrameSchema } from "../../src/ncp/frames/anchor-frame.js";
10
+ import { validateCapsFrame, type CapsFrame } from "../../src/ncp/frames/caps-frame.js";
11
+
12
+ const schemaV1: FrameSchema = {
13
+ fields: [
14
+ { name: "id", type: "uint64", semantic: "entity.id" },
15
+ { name: "name", type: "string", semantic: "entity.label" },
16
+ ],
17
+ };
18
+
19
+ const schemaV2: FrameSchema = {
20
+ fields: [
21
+ { name: "id", type: "uint64", semantic: "entity.id" },
22
+ { name: "name", type: "string", semantic: "entity.label" },
23
+ { name: "price", type: "decimal", semantic: "commerce.price.usd" },
24
+ ],
25
+ };
26
+
27
+ function makeValidCaps(overrides?: Partial<CapsFrame>): CapsFrame {
28
+ return {
29
+ frame: "0x04",
30
+ anchor_ref: computeAnchorId(schemaV1),
31
+ count: 2,
32
+ data: [{ id: 1, name: "Alpha" }, { id: 2, name: "Beta" }],
33
+ ...overrides,
34
+ };
35
+ }
36
+
37
+ describe("NCP-C: CapsFrame inline_anchor", () => {
38
+ // -----------------------------------------------------------------------
39
+ // NCP-C-05: inline_anchor Present (Valid)
40
+ // CapsFrame with inline_anchor containing valid AnchorFrame
41
+ // (correct anchor_id = JCS+SHA-256 of schema). Expected: Success
42
+ // -----------------------------------------------------------------------
43
+ it("NCP-C-05: valid inline_anchor passes validation", () => {
44
+ const anchorId = computeAnchorId(schemaV1);
45
+ const frame = makeValidCaps({
46
+ inline_anchor: { anchor_id: anchorId, schema: schemaV1, ttl: 3600 },
47
+ });
48
+ expect(() => validateCapsFrame(frame)).not.toThrow();
49
+ });
50
+
51
+ // -----------------------------------------------------------------------
52
+ // NCP-C-06: inline_anchor anchor_id Mismatch
53
+ // anchor_id != recomputed JCS+SHA-256 of schema → NCP-ANCHOR-SCHEMA-INVALID
54
+ // cache NOT updated
55
+ // -----------------------------------------------------------------------
56
+ it("NCP-C-06: inline_anchor with wrong anchor_id throws NCP-ANCHOR-SCHEMA-INVALID", () => {
57
+ const frame = makeValidCaps({
58
+ inline_anchor: {
59
+ anchor_id: "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
60
+ schema: schemaV1,
61
+ ttl: 3600,
62
+ },
63
+ });
64
+ expect(() => validateCapsFrame(frame)).toThrow(NcpError);
65
+ try {
66
+ validateCapsFrame(frame);
67
+ } catch (e) {
68
+ expect((e as NcpError).code).toBe("NCP-ANCHOR-SCHEMA-INVALID");
69
+ }
70
+ });
71
+
72
+ // -----------------------------------------------------------------------
73
+ // NCP-C-07: inline_anchor Auto-Update Flow
74
+ // Outer anchor_ref points to NEW anchor; inline_anchor carries its AnchorFrame.
75
+ // Old anchor retired from cache, new cached. Expected: Success
76
+ // -----------------------------------------------------------------------
77
+ it("NCP-C-07: inline_anchor with new schema version passes validation", () => {
78
+ const newAnchorId = computeAnchorId(schemaV2);
79
+ const frame = makeValidCaps({
80
+ anchor_ref: newAnchorId, // outer ref points to new anchor
81
+ inline_anchor: { anchor_id: newAnchorId, schema: schemaV2, ttl: 3600 },
82
+ });
83
+ expect(() => validateCapsFrame(frame)).not.toThrow();
84
+ // Cache update (retire old, store new) is a session-layer concern.
85
+ // validateCapsFrame confirms the inline_anchor is structurally valid.
86
+ expect(frame.inline_anchor!.anchor_id).toBe(newAnchorId);
87
+ });
88
+ });
@@ -0,0 +1,184 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
3
+ //
4
+ // NCP Test Cases — Group 5: Security & Edge Cases
5
+ // Covers: NCP-SEC-01 to NCP-SEC-05
6
+ // Source: test/ncp_test_cases.md §5
7
+
8
+ import { describe, it, expect } from "vitest";
9
+ import {
10
+ FrameType,
11
+ NcpError,
12
+ buildFlags,
13
+ EncodingTier,
14
+ } from "../../src/core/frame-header.js";
15
+ import { encodeFrame, decodeFrame } from "../../src/core/codecs/ncp-codec.js";
16
+ import { FrameRegistry } from "../../src/core/frame-registry.js";
17
+
18
+ // ===========================================================================
19
+ // NCP-SEC-01: Replay Attack
20
+ // ===========================================================================
21
+
22
+ describe("NCP-SEC-01: Replay Attack", () => {
23
+ // -----------------------------------------------------------------------
24
+ // Spec: §7.1 — Replay handled by TLS. Non-TLS: Agent SHOULD use nonce.
25
+ // Codec preserves nonce field for application-level dedup.
26
+ // -----------------------------------------------------------------------
27
+ it("preserves nonce field in round-trip", () => {
28
+ const query = {
29
+ frame: "0x10",
30
+ anchor_ref: "sha256:abc",
31
+ nonce: "550e8400-e29b-41d4-a716-446655440000",
32
+ limit: 10,
33
+ };
34
+
35
+ const encoded = encodeFrame(query, { frameType: FrameType.Query });
36
+ const result = decodeFrame(encoded);
37
+ const decoded = result.payload as typeof query;
38
+
39
+ expect(decoded.nonce).toBe("550e8400-e29b-41d4-a716-446655440000");
40
+ });
41
+ });
42
+
43
+ // ===========================================================================
44
+ // NCP-SEC-02: Encryption Bit Mismatch
45
+ // ===========================================================================
46
+
47
+ describe("NCP-SEC-02: Encryption Bit Mismatch", () => {
48
+ // -----------------------------------------------------------------------
49
+ // Spec: §7.4 — ENC=1 but payload is plain JSON → decryption failure
50
+ // At codec level: ENC=1 means payload has Nonce(12B) + ciphertext + Tag(16B).
51
+ // Plain JSON payload with ENC=1 will fail structure check.
52
+ // -----------------------------------------------------------------------
53
+ it("detects ENC flag mismatch (plain payload with ENC=1)", () => {
54
+ // Craft frame: ENC=1 flag but plain JSON payload
55
+ const payload = new TextEncoder().encode('{"frame":"0x01"}');
56
+ const flags = buildFlags({ encrypted: true }); // ENC=1
57
+
58
+ const header = new Uint8Array(4);
59
+ header[0] = FrameType.Anchor;
60
+ header[1] = flags;
61
+ const view = new DataView(header.buffer);
62
+ view.setUint16(2, payload.length, false);
63
+
64
+ const frame = new Uint8Array(header.length + payload.length);
65
+ frame.set(header);
66
+ frame.set(payload, header.length);
67
+
68
+ // Decode succeeds at codec level (JSON is valid bytes),
69
+ // but the ENC flag signals encryption was expected.
70
+ // Application layer should check header.isEncrypted and reject.
71
+ const result = decodeFrame(frame);
72
+ expect(result.header.isEncrypted).toBe(true);
73
+ // Application MUST reject: ENC=1 but got plaintext
74
+ });
75
+ });
76
+
77
+ // ===========================================================================
78
+ // NCP-SEC-03: Large Frame Attack
79
+ // ===========================================================================
80
+
81
+ describe("NCP-SEC-03: Large Frame Attack", () => {
82
+ // -----------------------------------------------------------------------
83
+ // Spec: §3.3 — EXT=1 max 4GB. Check memory limits before allocation.
84
+ // -----------------------------------------------------------------------
85
+ it("rejects frame exceeding memory limit", () => {
86
+ // Craft extended header declaring a huge payload
87
+ const header = new Uint8Array(8);
88
+ header[0] = FrameType.Anchor;
89
+ header[1] = 0x80; // EXT=1
90
+ const view = new DataView(header.buffer);
91
+ view.setUint32(2, 256 * 1024 * 1024, false); // 256MB
92
+ header[6] = 0;
93
+ header[7] = 0;
94
+
95
+ // Decode with small max_frame_payload
96
+ expect(() => decodeFrame(header, { maxFramePayload: 1024 * 1024 })).toThrow(
97
+ NcpError,
98
+ );
99
+ try {
100
+ decodeFrame(header, { maxFramePayload: 1024 * 1024 });
101
+ } catch (e) {
102
+ expect((e as NcpError).code).toBe("NCP-FRAME-PAYLOAD-TOO-LARGE");
103
+ }
104
+ });
105
+ });
106
+
107
+ // ===========================================================================
108
+ // NCP-SEC-04: Invalid Port Routing
109
+ // ===========================================================================
110
+
111
+ describe("NCP-SEC-04: Invalid Port Routing", () => {
112
+ // -----------------------------------------------------------------------
113
+ // Spec: §2.3 — Frame type determines protocol. Wrong protocol → unknown type.
114
+ // -----------------------------------------------------------------------
115
+ it("rejects NIP frame in NCP-only registry", () => {
116
+ // Create registry with only NCP frames
117
+ const registry = FrameRegistry.createDefault();
118
+
119
+ // NIP IdentFrame (0x20) — should be unknown if not registered
120
+ // Default registry doesn't include NIP frames beyond the basic set
121
+ // Let's test with a truly unregistered type
122
+ expect(() => registry.resolve(0x88)).toThrow(NcpError);
123
+ try {
124
+ registry.resolve(0x88);
125
+ } catch (e) {
126
+ expect((e as NcpError).code).toBe("NCP-FRAME-UNKNOWN-TYPE");
127
+ }
128
+ });
129
+
130
+ it("routes frame types to correct protocols", () => {
131
+ const registry = FrameRegistry.createDefault();
132
+
133
+ // NCP frames resolve to "ncp"
134
+ expect(registry.resolve(FrameType.Anchor).protocol).toBe("ncp");
135
+ // System frames resolve to "system"
136
+ expect(registry.resolve(FrameType.Error).protocol).toBe("system");
137
+ });
138
+ });
139
+
140
+ // ===========================================================================
141
+ // NCP-SEC-05: Protocol Version Check
142
+ // ===========================================================================
143
+
144
+ describe("NCP-SEC-05: Protocol Version Check", () => {
145
+ // -----------------------------------------------------------------------
146
+ // Spec: §2.6 — Client min_version > server version → NCP-VERSION-INCOMPATIBLE
147
+ // -----------------------------------------------------------------------
148
+ it("detects version incompatibility in HelloFrame", () => {
149
+ const hello = {
150
+ frame: "0x06",
151
+ nps_version: "0.5",
152
+ min_version: "0.5",
153
+ supported_encodings: ["json"],
154
+ supported_protocols: ["ncp"],
155
+ };
156
+
157
+ const serverVersion = "0.4";
158
+
159
+ // Simple version check: client's min > server's version
160
+ const clientMin = parseFloat(hello.min_version);
161
+ const serverMax = parseFloat(serverVersion);
162
+
163
+ expect(clientMin).toBeGreaterThan(serverMax);
164
+
165
+ // Server would return this error
166
+ const error = {
167
+ frame: "0xFE",
168
+ status: "NPS-PROTO-VERSION-INCOMPATIBLE",
169
+ error: "NCP-VERSION-INCOMPATIBLE",
170
+ message: "No compatible NPS version",
171
+ details: {
172
+ server_version: serverVersion,
173
+ client_min_version: hello.min_version,
174
+ },
175
+ };
176
+ expect(error.error).toBe("NCP-VERSION-INCOMPATIBLE");
177
+ });
178
+
179
+ it("accepts compatible versions", () => {
180
+ const clientMin = parseFloat("0.3");
181
+ const serverMax = parseFloat("0.4");
182
+ expect(serverMax).toBeGreaterThanOrEqual(clientMin); // compatible
183
+ });
184
+ });
@@ -0,0 +1,167 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
3
+ //
4
+ // NCP Test Cases — StreamManager window-based flow control
5
+ // Covers: NCP-S-07 through NCP-S-11
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 type { StreamFrame } from "../../src/ncp/frames/stream-frame.js";
12
+
13
+ function outgoing(
14
+ streamId: string,
15
+ seq: number,
16
+ data: unknown[],
17
+ opts?: { is_last?: boolean; window_size?: number },
18
+ ): StreamFrame {
19
+ return {
20
+ frame: "0x03",
21
+ stream_id: streamId,
22
+ seq,
23
+ is_last: opts?.is_last ?? false,
24
+ data,
25
+ window_size: opts?.window_size,
26
+ };
27
+ }
28
+
29
+ // ===========================================================================
30
+ // NCP-S-07: Initial Window
31
+ // ===========================================================================
32
+
33
+ describe("NCP-S-07: Initial window_size on seq=0 initialises remainingWindow", () => {
34
+ // -----------------------------------------------------------------------
35
+ // Spec: §3.3 — window_size=5 on seq=0 sets remainingWindow=5; each send
36
+ // decrements it by 1.
37
+ // -----------------------------------------------------------------------
38
+ it("initialises remainingWindow=5 and decrements on each send", () => {
39
+ const mgr = new StreamManager();
40
+
41
+ // Opening frame with window_size=5 — does not consume a window slot.
42
+ mgr.send(outgoing("s1", 0, [], { window_size: 5 }));
43
+ // After 0 sends: isPaused must be false; 5 sends should succeed.
44
+ expect(mgr.isPaused("s1")).toBe(false);
45
+
46
+ // Five sends should each succeed.
47
+ mgr.send(outgoing("s1", 1, ["a"]));
48
+ mgr.send(outgoing("s1", 2, ["b"]));
49
+ mgr.send(outgoing("s1", 3, ["c"]));
50
+ mgr.send(outgoing("s1", 4, ["d"]));
51
+ mgr.send(outgoing("s1", 5, ["e"]));
52
+
53
+ // Window now exhausted — next send must throw.
54
+ expect(() => mgr.send(outgoing("s1", 6, ["f"]))).toThrowError(NcpError);
55
+ try {
56
+ mgr.send(outgoing("s1", 6, ["f"]));
57
+ } catch (e) {
58
+ expect((e as NcpError).code).toBe("NCP-STREAM-WINDOW-OVERFLOW");
59
+ }
60
+ });
61
+ });
62
+
63
+ // ===========================================================================
64
+ // NCP-S-08: No Flow Control
65
+ // ===========================================================================
66
+
67
+ describe("NCP-S-08: No window_size means unlimited sends", () => {
68
+ // -----------------------------------------------------------------------
69
+ // Spec: §3.3 — absence of window_size disables flow control entirely.
70
+ // -----------------------------------------------------------------------
71
+ it("allows unlimited sends when no window_size is set", () => {
72
+ const mgr = new StreamManager();
73
+
74
+ // Opening frame without window_size.
75
+ mgr.send(outgoing("s1", 0, []));
76
+
77
+ // Many sends must all succeed without any exception.
78
+ for (let i = 1; i <= 100; i++) {
79
+ expect(() => mgr.send(outgoing("s1", i, ["x"]))).not.toThrow();
80
+ }
81
+ });
82
+ });
83
+
84
+ // ===========================================================================
85
+ // NCP-S-09: Reverse Refill
86
+ // ===========================================================================
87
+
88
+ describe("NCP-S-09: updateWindow replaces remainingWindow with new_size", () => {
89
+ // -----------------------------------------------------------------------
90
+ // Spec: §3.3 — a reverse-direction frame (data=[], window_size=N) refills
91
+ // the sender's window to exactly N.
92
+ // -----------------------------------------------------------------------
93
+ it("replaces exhausted window with new_size=10 and allows further sends", () => {
94
+ const mgr = new StreamManager();
95
+
96
+ mgr.send(outgoing("s1", 0, [], { window_size: 2 }));
97
+ mgr.send(outgoing("s1", 1, ["a"]));
98
+ mgr.send(outgoing("s1", 2, ["b"]));
99
+
100
+ // Window exhausted.
101
+ expect(() => mgr.send(outgoing("s1", 3, ["c"]))).toThrowError(NcpError);
102
+
103
+ // Reverse refill: updateWindow with new_size=10.
104
+ mgr.updateWindow("s1", 10);
105
+ expect(mgr.isPaused("s1")).toBe(false);
106
+
107
+ // Ten more sends should succeed.
108
+ for (let i = 3; i <= 12; i++) {
109
+ expect(() => mgr.send(outgoing("s1", i, ["x"]))).not.toThrow();
110
+ }
111
+
112
+ // 11th send after refill must throw again.
113
+ expect(() => mgr.send(outgoing("s1", 13, ["y"]))).toThrowError(NcpError);
114
+ });
115
+ });
116
+
117
+ // ===========================================================================
118
+ // NCP-S-10: Pause and Resume
119
+ // ===========================================================================
120
+
121
+ describe("NCP-S-10: window_size=0 sets pause state; non-zero refill resumes", () => {
122
+ // -----------------------------------------------------------------------
123
+ // Spec: §3.3 — receiver sends window_size=0 to pause the sender; a later
124
+ // non-zero window_size resumes it.
125
+ // -----------------------------------------------------------------------
126
+ it("sets pause when updateWindow(0) and clears it on non-zero refill", () => {
127
+ const mgr = new StreamManager();
128
+
129
+ mgr.send(outgoing("s1", 0, [], { window_size: 5 }));
130
+ expect(mgr.isPaused("s1")).toBe(false);
131
+
132
+ // Receiver signals pause.
133
+ mgr.updateWindow("s1", 0);
134
+ expect(mgr.isPaused("s1")).toBe(true);
135
+
136
+ // Receiver sends non-zero refill → resume.
137
+ mgr.updateWindow("s1", 5);
138
+ expect(mgr.isPaused("s1")).toBe(false);
139
+ });
140
+ });
141
+
142
+ // ===========================================================================
143
+ // NCP-S-11: Overflow
144
+ // ===========================================================================
145
+
146
+ describe("NCP-S-11: send when remainingWindow=0 throws NCP-STREAM-WINDOW-OVERFLOW", () => {
147
+ // -----------------------------------------------------------------------
148
+ // Spec: §3.3 — sending while window is exhausted is a protocol violation.
149
+ // -----------------------------------------------------------------------
150
+ it("throws NcpError with code NCP-STREAM-WINDOW-OVERFLOW", () => {
151
+ const mgr = new StreamManager();
152
+
153
+ mgr.send(outgoing("s1", 0, [], { window_size: 1 }));
154
+ mgr.send(outgoing("s1", 1, ["a"])); // consumes the only slot
155
+
156
+ // Window is now 0 — next send must throw with the correct error code.
157
+ let caught: unknown;
158
+ try {
159
+ mgr.send(outgoing("s1", 2, ["b"]));
160
+ } catch (e) {
161
+ caught = e;
162
+ }
163
+
164
+ expect(caught).toBeInstanceOf(NcpError);
165
+ expect((caught as NcpError).code).toBe("NCP-STREAM-WINDOW-OVERFLOW");
166
+ });
167
+ });