@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,327 @@
1
+ // Copyright 2026 INNO LOTUS PTY LTD
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { describe, expect, it } from "vitest";
5
+ import {
6
+ DEFAULT_HEADER_SIZE,
7
+ EXTENDED_HEADER_SIZE,
8
+ FrameFlags,
9
+ FrameHeader,
10
+ FrameType,
11
+ EncodingTier,
12
+ NpsCodecError,
13
+ NpsFrameError,
14
+ NpsAnchorNotFoundError,
15
+ NpsAnchorPoisonError,
16
+ NpsFrameCodec,
17
+ Tier1JsonCodec,
18
+ Tier2MsgPackCodec,
19
+ FrameRegistry,
20
+ AnchorFrameCache,
21
+ } from "../src/core/index.js";
22
+ import { AnchorFrame, CapsFrame, DiffFrame, ErrorFrame, StreamFrame } from "../src/ncp/frames.js";
23
+ import { registerNcpFrames } from "../src/ncp/registry.js";
24
+ import { createDefaultRegistry, createFullRegistry } from "../src/setup.js";
25
+
26
+ // ── FrameHeader ───────────────────────────────────────────────────────────────
27
+
28
+ describe("FrameHeader", () => {
29
+ it("parses a default (4-byte) header", () => {
30
+ const buf = new Uint8Array([0x01, 0x05, 0x00, 0x0A]); // ANCHOR, FINAL|JSON, length=10
31
+ const h = FrameHeader.parse(buf);
32
+ expect(h.frameType).toBe(FrameType.ANCHOR);
33
+ expect(h.isFinal).toBe(true);
34
+ expect(h.payloadLength).toBe(10);
35
+ expect(h.isExtended).toBe(false);
36
+ expect(h.headerSize).toBe(DEFAULT_HEADER_SIZE);
37
+ });
38
+
39
+ it("parses an extended (8-byte) header", () => {
40
+ const buf = new Uint8Array(8);
41
+ const view = new DataView(buf.buffer);
42
+ view.setUint8(0, FrameType.CAPS);
43
+ view.setUint8(1, FrameFlags.EXT | FrameFlags.TIER2_MSGPACK | FrameFlags.FINAL);
44
+ view.setUint16(2, 0, false); // reserved
45
+ view.setUint32(4, 100_000, false); // payload length
46
+ const h = FrameHeader.parse(buf);
47
+ expect(h.isExtended).toBe(true);
48
+ expect(h.headerSize).toBe(EXTENDED_HEADER_SIZE);
49
+ expect(h.payloadLength).toBe(100_000);
50
+ });
51
+
52
+ it("round-trips default header via toBytes()", () => {
53
+ const h = new FrameHeader(FrameType.ANCHOR, FrameFlags.FINAL | FrameFlags.TIER2_MSGPACK, 42);
54
+ const back = FrameHeader.parse(h.toBytes());
55
+ expect(back.frameType).toBe(FrameType.ANCHOR);
56
+ expect(back.payloadLength).toBe(42);
57
+ });
58
+
59
+ it("round-trips extended header via toBytes()", () => {
60
+ const h = new FrameHeader(FrameType.CAPS, FrameFlags.EXT | FrameFlags.FINAL | FrameFlags.TIER1_JSON, 70_000);
61
+ const back = FrameHeader.parse(h.toBytes());
62
+ expect(back.isExtended).toBe(true);
63
+ expect(back.payloadLength).toBe(70_000);
64
+ });
65
+
66
+ it("throws NpsFrameError for buffer too small", () => {
67
+ expect(() => FrameHeader.parse(new Uint8Array([0x01]))).toThrow(NpsFrameError);
68
+ });
69
+
70
+ it("throws NpsFrameError for extended header with short buffer", () => {
71
+ const buf = new Uint8Array([0x01, FrameFlags.EXT, 0x00, 0x00]); // EXT but only 4 bytes
72
+ expect(() => FrameHeader.parse(buf)).toThrow(NpsFrameError);
73
+ });
74
+
75
+ it("exposes encoding tier", () => {
76
+ const h = new FrameHeader(FrameType.ANCHOR, FrameFlags.TIER2_MSGPACK | FrameFlags.FINAL, 0);
77
+ expect(h.encodingTier).toBe(EncodingTier.MSGPACK);
78
+ });
79
+ });
80
+
81
+ // ── Exceptions ────────────────────────────────────────────────────────────────
82
+
83
+ describe("Exceptions", () => {
84
+ it("NpsAnchorNotFoundError carries anchorId", () => {
85
+ const err = new NpsAnchorNotFoundError("sha256:abc");
86
+ expect(err.anchorId).toBe("sha256:abc");
87
+ expect(err).toBeInstanceOf(NpsAnchorNotFoundError);
88
+ });
89
+
90
+ it("NpsAnchorPoisonError carries anchorId", () => {
91
+ const err = new NpsAnchorPoisonError("sha256:abc");
92
+ expect(err.anchorId).toBe("sha256:abc");
93
+ });
94
+ });
95
+
96
+ // ── FrameRegistry ─────────────────────────────────────────────────────────────
97
+
98
+ describe("FrameRegistry", () => {
99
+ it("resolves a registered frame class", () => {
100
+ const r = createDefaultRegistry();
101
+ const cls = r.resolve(FrameType.ANCHOR);
102
+ expect(cls).toBe(AnchorFrame);
103
+ });
104
+
105
+ it("throws NpsFrameError for unknown frame type", () => {
106
+ const r = new FrameRegistry();
107
+ expect(() => r.resolve(FrameType.ANCHOR)).toThrow(NpsFrameError);
108
+ });
109
+
110
+ it("createFullRegistry registers all 5 protocols", () => {
111
+ const r = createFullRegistry();
112
+ for (const ft of [
113
+ FrameType.ANCHOR, FrameType.QUERY, FrameType.IDENT,
114
+ FrameType.ANNOUNCE, FrameType.TASK,
115
+ ]) {
116
+ expect(() => r.resolve(ft)).not.toThrow();
117
+ }
118
+ });
119
+ });
120
+
121
+ // ── AnchorFrameCache ──────────────────────────────────────────────────────────
122
+
123
+ describe("AnchorFrameCache", () => {
124
+ const makeSchema = (fields = [{ name: "id", type: "uint64" }]) => ({ fields });
125
+
126
+ it("computeAnchorId is deterministic", () => {
127
+ const s = makeSchema();
128
+ expect(AnchorFrameCache.computeAnchorId(s)).toBe(AnchorFrameCache.computeAnchorId(s));
129
+ expect(AnchorFrameCache.computeAnchorId(s)).toMatch(/^sha256:[0-9a-f]{64}$/);
130
+ });
131
+
132
+ it("computeAnchorId is field-order independent", () => {
133
+ const s1 = { fields: [{ name: "a", type: "string" }, { name: "b", type: "uint64" }] };
134
+ const s2 = { fields: [{ name: "b", type: "uint64" }, { name: "a", type: "string" }] };
135
+ expect(AnchorFrameCache.computeAnchorId(s1)).toBe(AnchorFrameCache.computeAnchorId(s2));
136
+ });
137
+
138
+ it("set + get roundtrip", () => {
139
+ const cache = new AnchorFrameCache();
140
+ const schema = makeSchema();
141
+ const aid = AnchorFrameCache.computeAnchorId(schema);
142
+ const frame = new AnchorFrame(aid, schema, 3600);
143
+ cache.set(frame);
144
+ expect(cache.get(aid)).toBe(frame);
145
+ });
146
+
147
+ it("getRequired returns frame when present", () => {
148
+ const cache = new AnchorFrameCache();
149
+ const schema = makeSchema();
150
+ const aid = AnchorFrameCache.computeAnchorId(schema);
151
+ const frame = new AnchorFrame(aid, schema, 3600);
152
+ cache.set(frame);
153
+ expect(cache.getRequired(aid)).toBe(frame);
154
+ });
155
+
156
+ it("getRequired throws when missing", () => {
157
+ const cache = new AnchorFrameCache();
158
+ expect(() => cache.getRequired("sha256:" + "0".repeat(64))).toThrow(NpsAnchorNotFoundError);
159
+ });
160
+
161
+ it("get returns undefined after TTL expiry", () => {
162
+ const cache = new AnchorFrameCache();
163
+ let now = 0;
164
+ cache.clock = () => now;
165
+ const schema = makeSchema();
166
+ const aid = AnchorFrameCache.computeAnchorId(schema);
167
+ cache.set(new AnchorFrame(aid, schema, 10));
168
+ now = 11_000; // 11 seconds later
169
+ expect(cache.get(aid)).toBeUndefined();
170
+ });
171
+
172
+ it("idempotent set with same schema", () => {
173
+ const cache = new AnchorFrameCache();
174
+ const schema = makeSchema();
175
+ const aid = AnchorFrameCache.computeAnchorId(schema);
176
+ const frame = new AnchorFrame(aid, schema, 3600);
177
+ cache.set(frame);
178
+ cache.set(frame);
179
+ expect(cache.size).toBe(1);
180
+ });
181
+
182
+ it("poison detection raises NpsAnchorPoisonError", () => {
183
+ const cache = new AnchorFrameCache();
184
+ const schemaA = makeSchema([{ name: "id", type: "uint64" }]);
185
+ const schemaB = makeSchema([{ name: "price", type: "decimal" }]);
186
+ const aid = AnchorFrameCache.computeAnchorId(schemaA);
187
+ cache.set(new AnchorFrame(aid, schemaA, 3600));
188
+ expect(() => cache.set(new AnchorFrame(aid, schemaB, 3600))).toThrow(NpsAnchorPoisonError);
189
+ });
190
+
191
+ it("invalidate removes entry", () => {
192
+ const cache = new AnchorFrameCache();
193
+ const schema = makeSchema();
194
+ const aid = AnchorFrameCache.computeAnchorId(schema);
195
+ cache.set(new AnchorFrame(aid, schema, 3600));
196
+ cache.invalidate(aid);
197
+ expect(cache.get(aid)).toBeUndefined();
198
+ });
199
+
200
+ it("size evicts expired entries", () => {
201
+ const cache = new AnchorFrameCache();
202
+ let now = 0;
203
+ cache.clock = () => now;
204
+ const s1 = makeSchema([{ name: "id", type: "uint64" }]);
205
+ const s2 = makeSchema([{ name: "x", type: "string" }]);
206
+ cache.set(new AnchorFrame(AnchorFrameCache.computeAnchorId(s1), s1, 100));
207
+ cache.set(new AnchorFrame(AnchorFrameCache.computeAnchorId(s2), s2, 1));
208
+ now = 2_000; // s2 expired
209
+ expect(cache.size).toBe(1);
210
+ });
211
+ });
212
+
213
+ // ── NpsFrameCodec ─────────────────────────────────────────────────────────────
214
+
215
+ describe("NpsFrameCodec — NCP round-trips", () => {
216
+ const registry = createDefaultRegistry();
217
+ const codec = new NpsFrameCodec(registry);
218
+ const aid = "sha256:" + "a".repeat(64);
219
+ const schema = { fields: [{ name: "id", type: "uint64" }, { name: "name", type: "string" }] };
220
+
221
+ it("encodes/decodes AnchorFrame (MsgPack)", () => {
222
+ const frame = new AnchorFrame(aid, schema, 3600);
223
+ const out = codec.decode(codec.encode(frame)) as AnchorFrame;
224
+ expect(out).toBeInstanceOf(AnchorFrame);
225
+ expect(out.anchorId).toBe(aid);
226
+ expect(out.ttl).toBe(3600);
227
+ });
228
+
229
+ it("encodes/decodes AnchorFrame (JSON override)", () => {
230
+ const frame = new AnchorFrame(aid, schema, 7200);
231
+ const wire = codec.encode(frame, { overrideTier: EncodingTier.JSON });
232
+ const out = codec.decode(wire) as AnchorFrame;
233
+ expect(out.ttl).toBe(7200);
234
+ });
235
+
236
+ it("encodes/decodes DiffFrame", () => {
237
+ const frame = new DiffFrame(aid, 3, [{ op: "replace", path: "/name", value: "Bob" }], "ent:1");
238
+ const out = codec.decode(codec.encode(frame)) as DiffFrame;
239
+ expect(out).toBeInstanceOf(DiffFrame);
240
+ expect(out.baseSeq).toBe(3);
241
+ expect(out.patch[0]?.op).toBe("replace");
242
+ expect(out.entityId).toBe("ent:1");
243
+ });
244
+
245
+ it("encodes/decodes StreamFrame — non-final clears FINAL flag", () => {
246
+ const frame = new StreamFrame("s-1", 0, false, [{ id: 1 }]);
247
+ const wire = codec.encode(frame);
248
+ expect(NpsFrameCodec.peekHeader(wire).isFinal).toBe(false);
249
+ const out = codec.decode(wire) as StreamFrame;
250
+ expect(out.isLast).toBe(false);
251
+ });
252
+
253
+ it("encodes/decodes StreamFrame — final sets FINAL flag", () => {
254
+ const frame = new StreamFrame("s-1", 1, true, [{ id: 2 }], aid, 10);
255
+ const wire = codec.encode(frame);
256
+ expect(NpsFrameCodec.peekHeader(wire).isFinal).toBe(true);
257
+ const out = codec.decode(wire) as StreamFrame;
258
+ expect(out.isLast).toBe(true);
259
+ expect(out.windowSize).toBe(10);
260
+ });
261
+
262
+ it("encodes/decodes CapsFrame", () => {
263
+ const frame = new CapsFrame(aid, 2, [{ id: 1 }, { id: 2 }], "cursor:X", 100, true, "cl100k");
264
+ const out = codec.decode(codec.encode(frame)) as CapsFrame;
265
+ expect(out).toBeInstanceOf(CapsFrame);
266
+ expect(out.count).toBe(2);
267
+ expect(out.nextCursor).toBe("cursor:X");
268
+ expect(out.tokenizerUsed).toBe("cl100k");
269
+ });
270
+
271
+ it("encodes/decodes ErrorFrame", () => {
272
+ const frame = new ErrorFrame("NPS-SERVER-INTERNAL", "NCP-ANCHOR-NOT-FOUND", "missing anchor", { ref: aid });
273
+ const out = codec.decode(codec.encode(frame)) as ErrorFrame;
274
+ expect(out).toBeInstanceOf(ErrorFrame);
275
+ expect(out.status).toBe("NPS-SERVER-INTERNAL");
276
+ expect(out.message).toBe("missing anchor");
277
+ });
278
+
279
+ it("peekHeader decodes only the header", () => {
280
+ const frame = new AnchorFrame(aid, schema);
281
+ const wire = codec.encode(frame);
282
+ const header = NpsFrameCodec.peekHeader(wire);
283
+ expect(header.frameType).toBe(FrameType.ANCHOR);
284
+ });
285
+
286
+ it("throws NpsCodecError for unsupported tier", () => {
287
+ // @ts-expect-error intentional bad value
288
+ expect(() => codec["_selectCodec"](0x02)).toThrow(NpsCodecError);
289
+ });
290
+
291
+ it("throws NpsCodecError when payload exceeds maxPayload", () => {
292
+ const tiny = new NpsFrameCodec(registry, { maxPayload: 5 });
293
+ const frame = new AnchorFrame(aid, schema);
294
+ expect(() => tiny.encode(frame)).toThrow(NpsCodecError);
295
+ });
296
+
297
+ it("sets EXT flag when payload > 64 KiB", () => {
298
+ const large = new NpsFrameCodec(registry, { maxPayload: 200_000 });
299
+ const bigData = Array.from({ length: 400 }, (_, i) => ({ id: i, name: "x".repeat(200) }));
300
+ const frame = new CapsFrame(aid, bigData.length, bigData);
301
+ const wire = large.encode(frame, { overrideTier: EncodingTier.JSON });
302
+ expect(NpsFrameCodec.peekHeader(wire).isExtended).toBe(true);
303
+ });
304
+
305
+ it("Tier-1 JSON encode error wraps as NpsCodecError", () => {
306
+ const j = new Tier1JsonCodec();
307
+ const bad = { frameType: FrameType.ANCHOR, preferredTier: EncodingTier.JSON, toDict: () => ({ v: BigInt(1) }) };
308
+ // @ts-expect-error intentional bad frame
309
+ expect(() => j.encode(bad)).toThrow(NpsCodecError);
310
+ });
311
+
312
+ it("Tier-1 JSON decode error wraps as NpsCodecError", () => {
313
+ const j = new Tier1JsonCodec();
314
+ expect(() => j.decode(FrameType.ANCHOR, new Uint8Array([0xff, 0xfe]), registry)).toThrow(NpsCodecError);
315
+ });
316
+
317
+ it("Tier-2 MsgPack decode error wraps as NpsCodecError", () => {
318
+ const m = new Tier2MsgPackCodec();
319
+ // \xc1 is always-invalid in MsgPack
320
+ expect(() => m.decode(FrameType.ANCHOR, new Uint8Array([0xc1, 0xff, 0x00]), registry)).toThrow(NpsCodecError);
321
+ });
322
+
323
+ it("throws NpsFrameError for unknown frame type", () => {
324
+ const wire = new Uint8Array([0x99, FrameFlags.FINAL | FrameFlags.TIER1_JSON, 0x00, 0x02, 0x7b, 0x7d]);
325
+ expect(() => codec.decode(wire)).toThrow();
326
+ });
327
+ });
@@ -0,0 +1,107 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
3
+ //
4
+ // NCP Test Cases — NCP-D-05 through D-10: DiffFrame patch_format
5
+ // Source: test/ncp_test_cases.md §3.2
6
+
7
+ import { describe, it, expect } from "vitest";
8
+ import { NcpError, EncodingTier } from "../../src/core/frame-header.js";
9
+ import {
10
+ validateDiffFrame,
11
+ type DiffFrame,
12
+ } from "../../src/ncp/frames/diff-frame.js";
13
+
14
+ function makeJsonPatchFrame(overrides?: Partial<DiffFrame>): DiffFrame {
15
+ return {
16
+ frame: "0x02",
17
+ anchor_ref: "sha256:abc123",
18
+ base_seq: 0,
19
+ patch: [{ op: "replace", path: "/name", value: "updated" }],
20
+ ...overrides,
21
+ };
22
+ }
23
+
24
+ describe("NCP-D: DiffFrame patch_format", () => {
25
+ // -----------------------------------------------------------------------
26
+ // NCP-D-05: Default patch_format (json_patch)
27
+ // patch_format omitted on Tier-1 JSON frame → treat as json_patch → Success
28
+ // -----------------------------------------------------------------------
29
+ it("NCP-D-05: omitted patch_format on Tier-1 passes validation", () => {
30
+ const frame = makeJsonPatchFrame();
31
+ expect(frame.patch_format).toBeUndefined();
32
+ expect(() => validateDiffFrame(frame, EncodingTier.Json)).not.toThrow();
33
+ });
34
+
35
+ // -----------------------------------------------------------------------
36
+ // NCP-D-06: Explicit patch_format=json_patch
37
+ // json_patch on Tier-1 or Tier-2 → Success
38
+ // -----------------------------------------------------------------------
39
+ it("NCP-D-06: explicit patch_format=json_patch on Tier-1 passes", () => {
40
+ const frame = makeJsonPatchFrame({ patch_format: "json_patch" });
41
+ expect(() => validateDiffFrame(frame, EncodingTier.Json)).not.toThrow();
42
+ });
43
+
44
+ it("NCP-D-06: explicit patch_format=json_patch on Tier-2 passes", () => {
45
+ const frame = makeJsonPatchFrame({ patch_format: "json_patch" });
46
+ expect(() => validateDiffFrame(frame, EncodingTier.MsgPack)).not.toThrow();
47
+ });
48
+
49
+ // -----------------------------------------------------------------------
50
+ // NCP-D-07: binary_bitset on Tier-2 (supported)
51
+ // patch_format=binary_bitset on Tier-2 MsgPack → Success
52
+ // -----------------------------------------------------------------------
53
+ it("NCP-D-07: binary_bitset on Tier-2 MsgPack passes", () => {
54
+ const frame: DiffFrame = {
55
+ ...makeJsonPatchFrame(),
56
+ patch_format: "binary_bitset",
57
+ patch: new Uint8Array([0b00000011, 0x01, 0x05]), // bitset + packed values
58
+ };
59
+ expect(() => validateDiffFrame(frame, EncodingTier.MsgPack)).not.toThrow();
60
+ });
61
+
62
+ // -----------------------------------------------------------------------
63
+ // NCP-D-08: binary_bitset on Tier-1 (protocol forbids)
64
+ // Expected: NCP-DIFF-FORMAT-UNSUPPORTED
65
+ // -----------------------------------------------------------------------
66
+ it("NCP-D-08: binary_bitset on Tier-1 JSON throws NCP-DIFF-FORMAT-UNSUPPORTED", () => {
67
+ const frame = makeJsonPatchFrame({ patch_format: "binary_bitset" });
68
+ expect(() => validateDiffFrame(frame, EncodingTier.Json)).toThrow(NcpError);
69
+ try {
70
+ validateDiffFrame(frame, EncodingTier.Json);
71
+ } catch (e) {
72
+ expect((e as NcpError).code).toBe("NCP-DIFF-FORMAT-UNSUPPORTED");
73
+ }
74
+ });
75
+
76
+ // -----------------------------------------------------------------------
77
+ // NCP-D-09: binary_bitset when receiver opted out
78
+ // Simulated as Tier-1 (non-Tier-2) → NCP-DIFF-FORMAT-UNSUPPORTED
79
+ // -----------------------------------------------------------------------
80
+ it("NCP-D-09: binary_bitset when receiver opted out (non-Tier-2) throws NCP-DIFF-FORMAT-UNSUPPORTED", () => {
81
+ const frame = makeJsonPatchFrame({ patch_format: "binary_bitset" });
82
+ // Receiver did not advertise binary_bitset support — simulated as JSON tier
83
+ expect(() => validateDiffFrame(frame, EncodingTier.Json)).toThrow(NcpError);
84
+ try {
85
+ validateDiffFrame(frame, EncodingTier.Json);
86
+ } catch (e) {
87
+ expect((e as NcpError).code).toBe("NCP-DIFF-FORMAT-UNSUPPORTED");
88
+ }
89
+ });
90
+
91
+ // -----------------------------------------------------------------------
92
+ // NCP-D-10: Unknown patch_format
93
+ // patch_format="some_future_format" → NCP-DIFF-FORMAT-UNSUPPORTED
94
+ // -----------------------------------------------------------------------
95
+ it("NCP-D-10: unknown patch_format throws NCP-DIFF-FORMAT-UNSUPPORTED", () => {
96
+ // Cast to bypass TypeScript type check for runtime test
97
+ const frame = makeJsonPatchFrame({
98
+ patch_format: "some_future_format" as unknown as "json_patch",
99
+ });
100
+ expect(() => validateDiffFrame(frame, EncodingTier.Json)).toThrow(NcpError);
101
+ try {
102
+ validateDiffFrame(frame, EncodingTier.Json);
103
+ } catch (e) {
104
+ expect((e as NcpError).code).toBe("NCP-DIFF-FORMAT-UNSUPPORTED");
105
+ }
106
+ });
107
+ });
@@ -0,0 +1,93 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
3
+ //
4
+ // NCP Test Cases — NCP-E2E-01, E2E-02, E2E-03: E2E Encryption (Option A — reject ENC=1)
5
+ // Source: test/ncp_test_cases.md §8
6
+ //
7
+ // Option A conformance: TypeScript implementation rejects frames with ENC=1.
8
+ // Full AES-256-GCM / ChaCha20-Poly1305 is deferred to a future Option B iteration.
9
+
10
+ import { describe, it, expect } from "vitest";
11
+ import { NcpError, buildFlags, EncodingTier, parseFrameHeader } from "../../src/core/frame-header.js";
12
+
13
+ /**
14
+ * Check if a frame's ENC flag is set and throw NCP-ENC-NOT-NEGOTIATED
15
+ * if the session has no negotiated e2e_enc_algorithms.
16
+ */
17
+ function checkEncFlag(
18
+ flags: number,
19
+ sessionEncAlgorithms: string[],
20
+ ): void {
21
+ const ENC_BIT = 0x08;
22
+ if ((flags & ENC_BIT) !== 0 && sessionEncAlgorithms.length === 0) {
23
+ throw new NcpError(
24
+ "NCP-ENC-NOT-NEGOTIATED",
25
+ "Frame has ENC=1 but no e2e_enc_algorithms were negotiated",
26
+ );
27
+ }
28
+ }
29
+
30
+ describe("NCP-E2E: E2E Encryption Option A (reject ENC=1)", () => {
31
+ // -----------------------------------------------------------------------
32
+ // NCP-E2E-01: ENC=0 Default
33
+ // Frame with ENC=0 after handshake without e2e_enc_algorithms → Success
34
+ // -----------------------------------------------------------------------
35
+ it("NCP-E2E-01: ENC=0 frame with no negotiated algorithms is accepted", () => {
36
+ const flags = buildFlags({ tier: EncodingTier.Json, encrypted: false });
37
+ expect(() => checkEncFlag(flags, [])).not.toThrow();
38
+ });
39
+
40
+ it("NCP-E2E-01: ENC=0 flag is clear in default flags", () => {
41
+ const flags = buildFlags({ tier: EncodingTier.Json });
42
+ const ENC_BIT = 0x08;
43
+ expect(flags & ENC_BIT).toBe(0);
44
+ });
45
+
46
+ // -----------------------------------------------------------------------
47
+ // NCP-E2E-02: ENC=1 Without Negotiation
48
+ // Any frame with ENC=1 when session's e2e_enc_algorithms is empty
49
+ // → NCP-ENC-NOT-NEGOTIATED (frame dropped; no decryption attempted)
50
+ // -----------------------------------------------------------------------
51
+ it("NCP-E2E-02: ENC=1 frame without negotiated algorithms throws NCP-ENC-NOT-NEGOTIATED", () => {
52
+ const flags = buildFlags({ tier: EncodingTier.Json, encrypted: true });
53
+ expect(() => checkEncFlag(flags, [])).toThrow(NcpError);
54
+ try {
55
+ checkEncFlag(flags, []);
56
+ } catch (e) {
57
+ expect((e as NcpError).code).toBe("NCP-ENC-NOT-NEGOTIATED");
58
+ }
59
+ });
60
+
61
+ it("NCP-E2E-02: ENC=1 flag is set when encrypted=true", () => {
62
+ const flags = buildFlags({ tier: EncodingTier.Json, encrypted: true });
63
+ const ENC_BIT = 0x08;
64
+ expect(flags & ENC_BIT).not.toBe(0);
65
+ });
66
+
67
+ // -----------------------------------------------------------------------
68
+ // NCP-E2E-03: HelloFrame Omitted e2e_enc_algorithms; later frame has ENC=1
69
+ // → NCP-ENC-NOT-NEGOTIATED
70
+ // -----------------------------------------------------------------------
71
+ it("NCP-E2E-03: HelloFrame without e2e_enc_algorithms → later ENC=1 frame rejected", () => {
72
+ // Session built from HelloFrame that omitted e2e_enc_algorithms
73
+ const sessionAlgorithms: string[] = []; // as set by session from HelloFrame
74
+
75
+ const flags = buildFlags({ tier: EncodingTier.Json, encrypted: true });
76
+ expect(() => checkEncFlag(flags, sessionAlgorithms)).toThrow(NcpError);
77
+ try {
78
+ checkEncFlag(flags, sessionAlgorithms);
79
+ } catch (e) {
80
+ expect((e as NcpError).code).toBe("NCP-ENC-NOT-NEGOTIATED");
81
+ }
82
+ });
83
+
84
+ it("NCP-E2E-03: parseFrameHeader correctly exposes isEncrypted flag", () => {
85
+ const buf = new Uint8Array(4);
86
+ buf[0] = 0x06; // HelloFrame type
87
+ buf[1] = buildFlags({ tier: EncodingTier.Json, encrypted: true });
88
+ buf[2] = 0x00; // payload length high byte
89
+ buf[3] = 0x00; // payload length low byte
90
+ const header = parseFrameHeader(buf);
91
+ expect(header.isEncrypted).toBe(true);
92
+ });
93
+ });
@@ -0,0 +1,152 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
3
+ //
4
+ // NCP Test Cases — NCP-ERR-01, NCP-ERR-02, NCP-ERR-03: ErrorFrame (0xFE)
5
+ // Source: test/ncp_test_cases.md §3.5, §3.6
6
+
7
+ import { describe, it, expect } from "vitest";
8
+ import { isErrorFrame, type ErrorFrame } from "../../src/ncp/frames/error-frame.js";
9
+ import { NCP_ERROR_CODES } from "../../src/ncp/ncp-error-codes.js";
10
+
11
+ describe("NCP-ERR: ErrorFrame (0xFE)", () => {
12
+ // -----------------------------------------------------------------------
13
+ // NCP-ERR-01: Standard Error
14
+ // Spec: §4.7 — ErrorFrame carries NPS status + protocol error + message
15
+ // -----------------------------------------------------------------------
16
+ it("NCP-ERR-01: parses standard error with status, error, and message", () => {
17
+ const raw: ErrorFrame = {
18
+ frame: "0xFE",
19
+ status: "NPS-CLIENT-NOT-FOUND",
20
+ error: "NCP-ANCHOR-NOT-FOUND",
21
+ message: "Schema anchor not found in cache, please resend AnchorFrame",
22
+ };
23
+
24
+ expect(isErrorFrame(raw)).toBe(true);
25
+ expect(raw.status).toBe("NPS-CLIENT-NOT-FOUND");
26
+ expect(raw.error).toBe("NCP-ANCHOR-NOT-FOUND");
27
+ expect(raw.message).toContain("Schema anchor not found");
28
+ });
29
+
30
+ // -----------------------------------------------------------------------
31
+ // NCP-ERR-02: Nested Details
32
+ // Spec: §4.7 — details object contains structured data
33
+ // -----------------------------------------------------------------------
34
+ it("NCP-ERR-02: parses error with nested details object", () => {
35
+ const raw: ErrorFrame = {
36
+ frame: "0xFE",
37
+ status: "NPS-CLIENT-NOT-FOUND",
38
+ error: "NCP-ANCHOR-NOT-FOUND",
39
+ message: "Schema not found",
40
+ details: {
41
+ anchor_ref: "sha256:a3f9b2c1d4e5f6789012345678901234567890abcdef1234567890abcdef12",
42
+ retry_after_ms: 5000,
43
+ },
44
+ };
45
+
46
+ expect(isErrorFrame(raw)).toBe(true);
47
+ expect(raw.details).toBeDefined();
48
+ expect(raw.details!.anchor_ref).toBe(
49
+ "sha256:a3f9b2c1d4e5f6789012345678901234567890abcdef1234567890abcdef12",
50
+ );
51
+ expect(raw.details!.retry_after_ms).toBe(5000);
52
+ });
53
+
54
+ it("NCP-ERR-02: handles version incompatible error with server details", () => {
55
+ const raw: ErrorFrame = {
56
+ frame: "0xFE",
57
+ status: "NPS-PROTO-VERSION-INCOMPATIBLE",
58
+ error: "NCP-VERSION-INCOMPATIBLE",
59
+ message: "No compatible NPS version",
60
+ details: {
61
+ server_version: "0.4",
62
+ client_min_version: "0.5",
63
+ },
64
+ };
65
+
66
+ expect(isErrorFrame(raw)).toBe(true);
67
+ expect(raw.details!.server_version).toBe("0.4");
68
+ expect(raw.details!.client_min_version).toBe("0.5");
69
+ });
70
+
71
+ // -----------------------------------------------------------------------
72
+ // Type guard edge cases
73
+ // -----------------------------------------------------------------------
74
+ it("type guard rejects non-error objects", () => {
75
+ expect(isErrorFrame(null)).toBe(false);
76
+ expect(isErrorFrame({})).toBe(false);
77
+ expect(isErrorFrame({ frame: "0x01" })).toBe(false);
78
+ expect(isErrorFrame({ frame: "0xFE" })).toBe(false); // missing status + error
79
+ expect(isErrorFrame({ frame: "0xFE", status: "NPS-CLIENT-NOT-FOUND" })).toBe(false);
80
+ });
81
+
82
+ it("type guard accepts minimal valid error", () => {
83
+ expect(
84
+ isErrorFrame({
85
+ frame: "0xFE",
86
+ status: "NPS-CLIENT-BAD-FRAME",
87
+ error: "NCP-FRAME-UNKNOWN-TYPE",
88
+ }),
89
+ ).toBe(true);
90
+ });
91
+
92
+ // -----------------------------------------------------------------------
93
+ // NCP-ERR-03: v0.4 Error Code Roundtrip
94
+ // Spec: §3.6 — ErrorFrame carrying each of the 6 new v0.4 codes decodes cleanly
95
+ // New codes: NCP-ANCHOR-STALE, NCP-DIFF-FORMAT-UNSUPPORTED, NCP-VERSION-INCOMPATIBLE,
96
+ // NCP-STREAM-WINDOW-OVERFLOW, NCP-ENC-NOT-NEGOTIATED, NCP-ENC-AUTH-FAILED
97
+ // -----------------------------------------------------------------------
98
+ describe("NCP-ERR-03: v0.4 error code roundtrip", () => {
99
+ const v04Codes: Array<{ code: string; status: string; description: string }> = [
100
+ {
101
+ code: NCP_ERROR_CODES.NCP_ANCHOR_STALE,
102
+ status: "NPS-CLIENT-CONFLICT",
103
+ description: "Anchor is stale; server has a newer version",
104
+ },
105
+ {
106
+ code: NCP_ERROR_CODES.NCP_DIFF_FORMAT_UNSUPPORTED,
107
+ status: "NPS-CLIENT-BAD-FRAME",
108
+ description: "patch_format=binary_bitset not supported on this tier",
109
+ },
110
+ {
111
+ code: NCP_ERROR_CODES.NCP_VERSION_INCOMPATIBLE,
112
+ status: "NPS-PROTO-VERSION-INCOMPATIBLE",
113
+ description: "No compatible NPS version between client and server",
114
+ },
115
+ {
116
+ code: NCP_ERROR_CODES.NCP_STREAM_WINDOW_OVERFLOW,
117
+ status: "NPS-STREAM-LIMIT",
118
+ description: "Sender exceeded flow-control window",
119
+ },
120
+ {
121
+ code: NCP_ERROR_CODES.NCP_ENC_NOT_NEGOTIATED,
122
+ status: "NPS-CLIENT-BAD-FRAME",
123
+ description: "ENC=1 set but no encryption algorithms were negotiated",
124
+ },
125
+ {
126
+ code: NCP_ERROR_CODES.NCP_ENC_AUTH_FAILED,
127
+ status: "NPS-CLIENT-BAD-FRAME",
128
+ description: "E2E encryption auth-tag verification failed",
129
+ },
130
+ ];
131
+
132
+ for (const { code, status, description } of v04Codes) {
133
+ it(`roundtrips ErrorFrame with error="${code}"`, () => {
134
+ const raw: ErrorFrame = {
135
+ frame: "0xFE",
136
+ status,
137
+ error: code,
138
+ message: description,
139
+ };
140
+
141
+ // Encodes cleanly (type guard accepts)
142
+ expect(isErrorFrame(raw)).toBe(true);
143
+
144
+ // Decodes cleanly (fields preserved)
145
+ expect(raw.frame).toBe("0xFE");
146
+ expect(raw.status).toBe(status);
147
+ expect(raw.error).toBe(code);
148
+ expect(raw.message).toBe(description);
149
+ });
150
+ }
151
+ });
152
+ });