@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,242 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
3
+ //
4
+ // NCP Test Cases — AnchorCache
5
+ // Covers: NCP-A-03, A-05, A-06, NCP-CA-01 to CA-05
6
+ // Source: test/ncp_test_cases.md §3.1, §4
7
+
8
+ import { describe, it, expect } from "vitest";
9
+ import { NcpError } from "../../src/core/frame-header.js";
10
+ import { AnchorCache } from "../../src/core/anchor-cache.js";
11
+ import {
12
+ computeAnchorId,
13
+ type AnchorFrame,
14
+ type FrameSchema,
15
+ } from "../../src/ncp/frames/anchor-frame.js";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Helpers
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const schemaA: FrameSchema = {
22
+ fields: [
23
+ { name: "id", type: "uint64" },
24
+ { name: "name", type: "string" },
25
+ ],
26
+ };
27
+
28
+ const schemaB: FrameSchema = {
29
+ fields: [
30
+ { name: "id", type: "uint64" },
31
+ { name: "price", type: "decimal" },
32
+ ],
33
+ };
34
+
35
+ function makeAnchor(schema: FrameSchema, ttl?: number): AnchorFrame {
36
+ return {
37
+ frame: "0x01",
38
+ anchor_id: computeAnchorId(schema),
39
+ schema,
40
+ ttl: ttl ?? 3600,
41
+ };
42
+ }
43
+
44
+ // ===========================================================================
45
+ // NCP-A-03: Anchor Poisoning
46
+ // ===========================================================================
47
+
48
+ describe("NCP-A-03: Anchor Poisoning", () => {
49
+ // -----------------------------------------------------------------------
50
+ // Spec: §7.2 — Same anchor_id with different schema → NCP-ANCHOR-ID-MISMATCH
51
+ // -----------------------------------------------------------------------
52
+ it("detects anchor poisoning (same ID, different schema)", () => {
53
+ const cache = new AnchorCache();
54
+ const anchor = makeAnchor(schemaA);
55
+ cache.set(anchor);
56
+
57
+ // Craft a poisoned frame: same anchor_id but different schema
58
+ const poisoned: AnchorFrame = {
59
+ frame: "0x01",
60
+ anchor_id: anchor.anchor_id, // same ID
61
+ schema: schemaB, // different schema!
62
+ ttl: 3600,
63
+ };
64
+
65
+ expect(() => cache.set(poisoned)).toThrow(NcpError);
66
+ try {
67
+ cache.set(poisoned);
68
+ } catch (e) {
69
+ expect((e as NcpError).code).toBe("NCP-ANCHOR-ID-MISMATCH");
70
+ }
71
+ });
72
+
73
+ it("allows idempotent set with same schema", () => {
74
+ const cache = new AnchorCache();
75
+ const anchor = makeAnchor(schemaA);
76
+ cache.set(anchor);
77
+ expect(() => cache.set(anchor)).not.toThrow(); // same schema — fine
78
+ expect(cache.size).toBe(1);
79
+ });
80
+ });
81
+
82
+ // ===========================================================================
83
+ // NCP-A-05: Zero TTL
84
+ // ===========================================================================
85
+
86
+ describe("NCP-A-05: Zero TTL", () => {
87
+ // -----------------------------------------------------------------------
88
+ // Spec: §4.1 — ttl=0 means don't cache, use once
89
+ // -----------------------------------------------------------------------
90
+ it("does not cache frames with ttl=0", () => {
91
+ const cache = new AnchorCache();
92
+ const anchor = makeAnchor(schemaA, 0); // ttl=0
93
+ cache.set(anchor);
94
+
95
+ expect(cache.size).toBe(0);
96
+ expect(cache.get(anchor.anchor_id)).toBeNull();
97
+ });
98
+ });
99
+
100
+ // ===========================================================================
101
+ // NCP-A-06: TTL Expiry
102
+ // ===========================================================================
103
+
104
+ describe("NCP-A-06: TTL Expiry", () => {
105
+ // -----------------------------------------------------------------------
106
+ // Spec: §5.3 — Expired anchors return not-found
107
+ // -----------------------------------------------------------------------
108
+ it("expires anchors after TTL", () => {
109
+ let now = 1000;
110
+ const cache = new AnchorCache({ getNow: () => now });
111
+
112
+ const anchor = makeAnchor(schemaA, 1); // ttl=1 second
113
+ cache.set(anchor);
114
+
115
+ // Still valid
116
+ expect(cache.get(anchor.anchor_id)).not.toBeNull();
117
+
118
+ // Advance time past TTL
119
+ now = 3000; // 2 seconds later
120
+ expect(cache.get(anchor.anchor_id)).toBeNull();
121
+ });
122
+
123
+ it("getRequired throws NCP-ANCHOR-NOT-FOUND after expiry", () => {
124
+ let now = 1000;
125
+ const cache = new AnchorCache({ getNow: () => now });
126
+
127
+ const anchor = makeAnchor(schemaA, 1);
128
+ cache.set(anchor);
129
+
130
+ now = 3000;
131
+ expect(() => cache.getRequired(anchor.anchor_id)).toThrow(NcpError);
132
+ try {
133
+ cache.getRequired(anchor.anchor_id);
134
+ } catch (e) {
135
+ expect((e as NcpError).code).toBe("NCP-ANCHOR-NOT-FOUND");
136
+ }
137
+ });
138
+ });
139
+
140
+ // ===========================================================================
141
+ // Group 4: Schema Caching
142
+ // ===========================================================================
143
+
144
+ describe("Group 4: Schema Caching", () => {
145
+ // -----------------------------------------------------------------------
146
+ // NCP-CA-01: Cache Hit
147
+ // Spec: §5.3 — anchor_ref already in local cache
148
+ // -----------------------------------------------------------------------
149
+ it("NCP-CA-01: returns cached schema on hit", () => {
150
+ const cache = new AnchorCache();
151
+ const anchor = makeAnchor(schemaA);
152
+ cache.set(anchor);
153
+
154
+ const result = cache.get(anchor.anchor_id);
155
+ expect(result).not.toBeNull();
156
+ expect(result!.anchor_id).toBe(anchor.anchor_id);
157
+ expect(result!.schema.fields).toHaveLength(2);
158
+ });
159
+
160
+ // -----------------------------------------------------------------------
161
+ // NCP-CA-02: Cache Miss (Local)
162
+ // Spec: §5.4 — Unknown anchor_ref → trigger fetch
163
+ // -----------------------------------------------------------------------
164
+ it("NCP-CA-02: returns null for unknown anchor_ref", () => {
165
+ const cache = new AnchorCache();
166
+ expect(cache.get("sha256:unknown")).toBeNull();
167
+ });
168
+
169
+ // -----------------------------------------------------------------------
170
+ // NCP-CA-03: Cache Miss (Server)
171
+ // Spec: §5.4.2 — Server returns NCP-ANCHOR-NOT-FOUND
172
+ // -----------------------------------------------------------------------
173
+ it("NCP-CA-03: getRequired throws NCP-ANCHOR-NOT-FOUND for unknown ref", () => {
174
+ const cache = new AnchorCache();
175
+ expect(() => cache.getRequired("sha256:fabricated")).toThrow(NcpError);
176
+ try {
177
+ cache.getRequired("sha256:fabricated");
178
+ } catch (e) {
179
+ expect((e as NcpError).code).toBe("NCP-ANCHOR-NOT-FOUND");
180
+ }
181
+ });
182
+
183
+ // -----------------------------------------------------------------------
184
+ // NCP-CA-04: LRU Eviction
185
+ // Spec: §9 — Cache limit 1000, LRU eviction
186
+ // -----------------------------------------------------------------------
187
+ it("NCP-CA-04: evicts LRU entry when cache is full", () => {
188
+ let now = 1000;
189
+ const cache = new AnchorCache({ maxSize: 3, getNow: () => now });
190
+
191
+ // Fill cache with 3 entries at different times
192
+ const anchors = Array.from({ length: 3 }, (_, i) => {
193
+ const schema: FrameSchema = {
194
+ fields: [{ name: `field_${i}`, type: "string" }],
195
+ };
196
+ return makeAnchor(schema);
197
+ });
198
+ now = 1000; cache.set(anchors[0]!);
199
+ now = 2000; cache.set(anchors[1]!);
200
+ now = 3000; cache.set(anchors[2]!);
201
+ expect(cache.size).toBe(3);
202
+
203
+ // Access first anchor to make it most recently used
204
+ now = 4000;
205
+ cache.get(anchors[0]!.anchor_id);
206
+
207
+ // Add 4th entry — should evict anchors[1] (lastAccessed=2000, the oldest)
208
+ now = 5000;
209
+ const newSchema: FrameSchema = {
210
+ fields: [{ name: "field_new", type: "string" }],
211
+ };
212
+ const newAnchor = makeAnchor(newSchema);
213
+ cache.set(newAnchor);
214
+
215
+ expect(cache.size).toBe(3);
216
+ expect(cache.get(anchors[0]!.anchor_id)).not.toBeNull(); // still here (accessed at 4000)
217
+ expect(cache.get(anchors[1]!.anchor_id)).toBeNull(); // evicted (LRU at 2000)
218
+ expect(cache.get(newAnchor.anchor_id)).not.toBeNull(); // newly added
219
+ });
220
+
221
+ // -----------------------------------------------------------------------
222
+ // NCP-CA-05: Schema Update
223
+ // Spec: §5.4.1 — Same name, different anchor_id can coexist
224
+ // -----------------------------------------------------------------------
225
+ it("NCP-CA-05: stores multiple schema versions by anchor_id", () => {
226
+ const cache = new AnchorCache();
227
+
228
+ // v1
229
+ const anchorV1 = makeAnchor(schemaA);
230
+ cache.set(anchorV1);
231
+
232
+ // v2 (different schema → different anchor_id)
233
+ const anchorV2 = makeAnchor(schemaB);
234
+ cache.set(anchorV2);
235
+
236
+ // Both should coexist
237
+ expect(cache.get(anchorV1.anchor_id)).not.toBeNull();
238
+ expect(cache.get(anchorV2.anchor_id)).not.toBeNull();
239
+ expect(anchorV1.anchor_id).not.toBe(anchorV2.anchor_id);
240
+ expect(cache.size).toBe(2);
241
+ });
242
+ });
@@ -0,0 +1,205 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
3
+ //
4
+ // NCP Test Cases — Codec dispatcher + MsgPack
5
+ // Covers: NCP-F-04, F-09, NCP-E-02, E-04, E-05, E-06
6
+ // Source: test/ncp_test_cases.md §1, §2
7
+
8
+ import { describe, it, expect } from "vitest";
9
+ import { EncodingTier, FrameType, NcpError } from "../../src/core/frame-header.js";
10
+ import { encodeFrame, decodeFrame } from "../../src/core/codecs/ncp-codec.js";
11
+ import { computeAnchorId, type FrameSchema } from "../../src/ncp/frames/anchor-frame.js";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Helpers
15
+ // ---------------------------------------------------------------------------
16
+
17
+ const testSchema: FrameSchema = {
18
+ fields: [
19
+ { name: "id", type: "uint64" },
20
+ { name: "name", type: "string" },
21
+ ],
22
+ };
23
+
24
+ // ===========================================================================
25
+ // NCP-F-04: EXT Flag Mismatch (High) — payload exceeds default max
26
+ // ===========================================================================
27
+
28
+ describe("NCP-F-04 / NCP-F-09: Payload size enforcement", () => {
29
+ // -----------------------------------------------------------------------
30
+ // NCP-F-09: Max Payload Enforced
31
+ // Spec: §3.3 — Negotiated max_frame_payload
32
+ // -----------------------------------------------------------------------
33
+ it("NCP-F-09: rejects payload exceeding negotiated max", () => {
34
+ // Encode a small frame, then decode with a tiny max
35
+ const frame = encodeFrame(
36
+ { frame: "0x01", anchor_id: "sha256:abc", schema: testSchema },
37
+ { frameType: FrameType.Anchor },
38
+ );
39
+
40
+ expect(() => decodeFrame(frame, { maxFramePayload: 10 })).toThrow(NcpError);
41
+ try {
42
+ decodeFrame(frame, { maxFramePayload: 10 });
43
+ } catch (e) {
44
+ expect((e as NcpError).code).toBe("NCP-FRAME-PAYLOAD-TOO-LARGE");
45
+ }
46
+ });
47
+
48
+ it("NCP-F-09: accepts payload within negotiated max", () => {
49
+ const frame = encodeFrame(
50
+ { frame: "0x01", data: "small" },
51
+ { frameType: FrameType.Anchor },
52
+ );
53
+ expect(() => decodeFrame(frame, { maxFramePayload: 65535 })).not.toThrow();
54
+ });
55
+
56
+ // NCP-F-04: The codec auto-upgrades to extended header for large payloads
57
+ it("NCP-F-04: auto-extends header for payload > 64KB", () => {
58
+ const largeData = { frame: "0x03", data: "x".repeat(70_000) };
59
+ const frame = encodeFrame(largeData, { frameType: FrameType.Stream });
60
+
61
+ // Should have used extended header
62
+ expect(frame[1]! & 0x80).toBe(0x80); // EXT bit set
63
+
64
+ // Decode with large enough max
65
+ const result = decodeFrame(frame, { maxFramePayload: 0xffffffff });
66
+ expect((result.payload as { data: string }).data).toHaveLength(70_000);
67
+ });
68
+ });
69
+
70
+ // ===========================================================================
71
+ // NCP-E-02: Tier-2 MsgPack (Valid)
72
+ // ===========================================================================
73
+
74
+ describe("NCP-E-02 / E-04: Tier-2 MsgPack", () => {
75
+ // -----------------------------------------------------------------------
76
+ // NCP-E-02: Tier-2 MsgPack (Valid)
77
+ // Spec: §3.2, §8 — Flags T0/T1 = 01
78
+ // -----------------------------------------------------------------------
79
+ it("NCP-E-02: round-trips frame via MsgPack encoding", () => {
80
+ const anchor = {
81
+ frame: "0x01",
82
+ anchor_id: computeAnchorId(testSchema),
83
+ schema: testSchema,
84
+ ttl: 3600,
85
+ };
86
+
87
+ const encoded = encodeFrame(anchor, {
88
+ frameType: FrameType.Anchor,
89
+ tier: EncodingTier.MsgPack,
90
+ });
91
+ const result = decodeFrame(encoded);
92
+
93
+ expect(result.header.tier).toBe(EncodingTier.MsgPack);
94
+ const decoded = result.payload as typeof anchor;
95
+ expect(decoded.frame).toBe("0x01");
96
+ expect(decoded.ttl).toBe(3600);
97
+ });
98
+
99
+ // -----------------------------------------------------------------------
100
+ // NCP-E-04: Tier-2 MsgPack (Malformed)
101
+ // Spec: §3.2, §8 — Invalid MsgPack → NPS-CLIENT-BAD-FRAME
102
+ // -----------------------------------------------------------------------
103
+ it("NCP-E-04: rejects malformed MsgPack payload", () => {
104
+ // Craft a frame with MsgPack tier flag but garbage payload
105
+ const header = new Uint8Array([
106
+ FrameType.Anchor, // frame type
107
+ 0x01, // flags: Tier-2 MsgPack
108
+ 0x00,
109
+ 0x04, // payload length: 4
110
+ ]);
111
+ const garbage = new Uint8Array([0xff, 0xfe, 0xfd, 0xfc]);
112
+ const frame = new Uint8Array(header.length + garbage.length);
113
+ frame.set(header);
114
+ frame.set(garbage, header.length);
115
+
116
+ expect(() => decodeFrame(frame)).toThrow(NcpError);
117
+ try {
118
+ decodeFrame(frame);
119
+ } catch (e) {
120
+ expect((e as NcpError).code).toBe("NPS-CLIENT-BAD-FRAME");
121
+ }
122
+ });
123
+ });
124
+
125
+ // ===========================================================================
126
+ // NCP-E-05: Reserved Tier
127
+ // ===========================================================================
128
+
129
+ describe("NCP-E-05: Reserved encoding tier", () => {
130
+ // -----------------------------------------------------------------------
131
+ // NCP-E-05: Reserved Tier (T3/T4)
132
+ // Spec: §3.2, §8 — T0/T1 = 10 or 11 → NCP-ENCODING-UNSUPPORTED
133
+ // -----------------------------------------------------------------------
134
+ it("NCP-E-05: rejects reserved encoding tier 0x02", () => {
135
+ // Craft header with tier bits = 10 (0x02)
136
+ const header = new Uint8Array([
137
+ FrameType.Anchor,
138
+ 0x02, // flags: tier = 10 (reserved)
139
+ 0x00,
140
+ 0x02, // payload: 2 bytes
141
+ ]);
142
+ const payload = new Uint8Array([0x80, 0x00]); // dummy
143
+ const frame = new Uint8Array(header.length + payload.length);
144
+ frame.set(header);
145
+ frame.set(payload, header.length);
146
+
147
+ expect(() => decodeFrame(frame)).toThrow(NcpError);
148
+ try {
149
+ decodeFrame(frame);
150
+ } catch (e) {
151
+ expect((e as NcpError).code).toBe("NCP-ENCODING-UNSUPPORTED");
152
+ }
153
+ });
154
+
155
+ it("NCP-E-05: rejects reserved encoding tier 0x03", () => {
156
+ const header = new Uint8Array([
157
+ FrameType.Anchor,
158
+ 0x03, // flags: tier = 11 (reserved)
159
+ 0x00,
160
+ 0x02,
161
+ ]);
162
+ const payload = new Uint8Array([0x80, 0x00]);
163
+ const frame = new Uint8Array(header.length + payload.length);
164
+ frame.set(header);
165
+ frame.set(payload, header.length);
166
+
167
+ expect(() => decodeFrame(frame)).toThrow(NcpError);
168
+ try {
169
+ decodeFrame(frame);
170
+ } catch (e) {
171
+ expect((e as NcpError).code).toBe("NCP-ENCODING-UNSUPPORTED");
172
+ }
173
+ });
174
+ });
175
+
176
+ // ===========================================================================
177
+ // NCP-E-06: Encoding Switch
178
+ // ===========================================================================
179
+
180
+ describe("NCP-E-06: Mid-session encoding switch", () => {
181
+ // -----------------------------------------------------------------------
182
+ // NCP-E-06: Encoding Switch
183
+ // Spec: §8 — Different frames MAY use different tiers
184
+ // -----------------------------------------------------------------------
185
+ it("NCP-E-06: decodes JSON then MsgPack frames back-to-back", () => {
186
+ const data = { frame: "0x04", anchor_ref: "sha256:abc", count: 0, data: [] };
187
+
188
+ const jsonFrame = encodeFrame(data, {
189
+ frameType: FrameType.Caps,
190
+ tier: EncodingTier.Json,
191
+ });
192
+ const msgpackFrame = encodeFrame(data, {
193
+ frameType: FrameType.Caps,
194
+ tier: EncodingTier.MsgPack,
195
+ });
196
+
197
+ const result1 = decodeFrame(jsonFrame);
198
+ expect(result1.header.tier).toBe(EncodingTier.Json);
199
+ expect((result1.payload as { count: number }).count).toBe(0);
200
+
201
+ const result2 = decodeFrame(msgpackFrame);
202
+ expect(result2.header.tier).toBe(EncodingTier.MsgPack);
203
+ expect((result2.payload as { count: number }).count).toBe(0);
204
+ });
205
+ });
@@ -0,0 +1,46 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
3
+ //
4
+ // NCP Test Cases — NCP-F-08: Unknown Frame Type
5
+ // Source: test/ncp_test_cases.md §1
6
+
7
+ import { describe, it, expect } from "vitest";
8
+ import { FrameType, NcpError } from "../../src/core/frame-header.js";
9
+ import { FrameRegistry } from "../../src/core/frame-registry.js";
10
+
11
+ describe("NCP-F-08: Unknown Frame Type", () => {
12
+ const registry = FrameRegistry.createDefault();
13
+
14
+ // -----------------------------------------------------------------------
15
+ // NCP-F-08: Unknown Frame Type
16
+ // Spec: §2.3 — Frame type routing table
17
+ // -----------------------------------------------------------------------
18
+ it("rejects unregistered frame type 0x88", () => {
19
+ expect(() => registry.resolve(0x88)).toThrow(NcpError);
20
+ try {
21
+ registry.resolve(0x88);
22
+ expect.unreachable("should have thrown");
23
+ } catch (e) {
24
+ expect((e as NcpError).code).toBe("NCP-FRAME-UNKNOWN-TYPE");
25
+ }
26
+ });
27
+
28
+ it("resolves registered NCP frame types", () => {
29
+ expect(registry.resolve(FrameType.Anchor).name).toBe("AnchorFrame");
30
+ expect(registry.resolve(FrameType.Diff).name).toBe("DiffFrame");
31
+ expect(registry.resolve(FrameType.Stream).name).toBe("StreamFrame");
32
+ expect(registry.resolve(FrameType.Caps).name).toBe("CapsFrame");
33
+ expect(registry.resolve(FrameType.Hello).name).toBe("HelloFrame");
34
+ expect(registry.resolve(FrameType.Error).name).toBe("ErrorFrame");
35
+ });
36
+
37
+ it("resolves protocol for frame types", () => {
38
+ expect(registry.resolve(FrameType.Anchor).protocol).toBe("ncp");
39
+ expect(registry.resolve(FrameType.Error).protocol).toBe("system");
40
+ });
41
+
42
+ it("has() returns false for unregistered types", () => {
43
+ expect(registry.has(0x88)).toBe(false);
44
+ expect(registry.has(FrameType.Anchor)).toBe(true);
45
+ });
46
+ });