@labacacia/nps-sdk 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +33 -0
- package/LICENSE +170 -0
- package/NOTICE +7 -0
- package/README.md +153 -0
- package/dist/codec-CmHeovTV.d.cts +120 -0
- package/dist/codec-CmHeovTV.d.ts +120 -0
- package/dist/core/anchor-cache.d.ts +42 -0
- package/dist/core/anchor-cache.d.ts.map +1 -0
- package/dist/core/anchor-cache.js +104 -0
- package/dist/core/anchor-cache.js.map +1 -0
- package/dist/core/cache.d.ts +14 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +80 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/canonical-json.d.ts +12 -0
- package/dist/core/canonical-json.d.ts.map +1 -0
- package/dist/core/canonical-json.js +44 -0
- package/dist/core/canonical-json.js.map +1 -0
- package/dist/core/codec.d.ts +32 -0
- package/dist/core/codec.d.ts.map +1 -0
- package/dist/core/codec.js +119 -0
- package/dist/core/codec.js.map +1 -0
- package/dist/core/codecs/index.d.ts +4 -0
- package/dist/core/codecs/index.d.ts.map +1 -0
- package/dist/core/codecs/index.js +6 -0
- package/dist/core/codecs/index.js.map +1 -0
- package/dist/core/codecs/ncp-codec.d.ts +39 -0
- package/dist/core/codecs/ncp-codec.d.ts.map +1 -0
- package/dist/core/codecs/ncp-codec.js +93 -0
- package/dist/core/codecs/ncp-codec.js.map +1 -0
- package/dist/core/codecs/tier1-json-codec.d.ts +10 -0
- package/dist/core/codecs/tier1-json-codec.d.ts.map +1 -0
- package/dist/core/codecs/tier1-json-codec.js +28 -0
- package/dist/core/codecs/tier1-json-codec.js.map +1 -0
- package/dist/core/codecs/tier2-msgpack-codec.d.ts +10 -0
- package/dist/core/codecs/tier2-msgpack-codec.d.ts.map +1 -0
- package/dist/core/codecs/tier2-msgpack-codec.js +26 -0
- package/dist/core/codecs/tier2-msgpack-codec.js.map +1 -0
- package/dist/core/crypto-provider.d.ts +31 -0
- package/dist/core/crypto-provider.d.ts.map +1 -0
- package/dist/core/crypto-provider.js +10 -0
- package/dist/core/crypto-provider.js.map +1 -0
- package/dist/core/exceptions.d.ts +27 -0
- package/dist/core/exceptions.d.ts.map +1 -0
- package/dist/core/exceptions.js +52 -0
- package/dist/core/exceptions.js.map +1 -0
- package/dist/core/frame-header.d.ts +87 -0
- package/dist/core/frame-header.d.ts.map +1 -0
- package/dist/core/frame-header.js +185 -0
- package/dist/core/frame-header.js.map +1 -0
- package/dist/core/frame-registry.d.ts +35 -0
- package/dist/core/frame-registry.d.ts.map +1 -0
- package/dist/core/frame-registry.js +63 -0
- package/dist/core/frame-registry.js.map +1 -0
- package/dist/core/frames.d.ts +80 -0
- package/dist/core/frames.d.ts.map +1 -0
- package/dist/core/frames.js +153 -0
- package/dist/core/frames.js.map +1 -0
- package/dist/core/index.cjs +371 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +41 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +10 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/registry.d.ts +11 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +17 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/status-codes.d.ts +28 -0
- package/dist/core/status-codes.d.ts.map +1 -0
- package/dist/core/status-codes.js +38 -0
- package/dist/core/status-codes.js.map +1 -0
- package/dist/frames-B3qLdl_g.d.cts +77 -0
- package/dist/frames-Ff7-ZPUl.d.ts +77 -0
- package/dist/index.cjs +1556 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/ncp/frames/anchor-frame.d.ts +29 -0
- package/dist/ncp/frames/anchor-frame.d.ts.map +1 -0
- package/dist/ncp/frames/anchor-frame.js +54 -0
- package/dist/ncp/frames/anchor-frame.js.map +1 -0
- package/dist/ncp/frames/caps-frame.d.ts +29 -0
- package/dist/ncp/frames/caps-frame.d.ts.map +1 -0
- package/dist/ncp/frames/caps-frame.js +29 -0
- package/dist/ncp/frames/caps-frame.js.map +1 -0
- package/dist/ncp/frames/diff-frame.d.ts +32 -0
- package/dist/ncp/frames/diff-frame.d.ts.map +1 -0
- package/dist/ncp/frames/diff-frame.js +37 -0
- package/dist/ncp/frames/diff-frame.js.map +1 -0
- package/dist/ncp/frames/error-frame.d.ts +16 -0
- package/dist/ncp/frames/error-frame.d.ts.map +1 -0
- package/dist/ncp/frames/error-frame.js +13 -0
- package/dist/ncp/frames/error-frame.js.map +1 -0
- package/dist/ncp/frames/hello-frame.d.ts +21 -0
- package/dist/ncp/frames/hello-frame.d.ts.map +1 -0
- package/dist/ncp/frames/hello-frame.js +25 -0
- package/dist/ncp/frames/hello-frame.js.map +1 -0
- package/dist/ncp/frames/stream-frame.d.ts +16 -0
- package/dist/ncp/frames/stream-frame.d.ts.map +1 -0
- package/dist/ncp/frames/stream-frame.js +18 -0
- package/dist/ncp/frames/stream-frame.js.map +1 -0
- package/dist/ncp/frames.d.ts +76 -0
- package/dist/ncp/frames.d.ts.map +1 -0
- package/dist/ncp/frames.js +147 -0
- package/dist/ncp/frames.js.map +1 -0
- package/dist/ncp/handshake.d.ts +30 -0
- package/dist/ncp/handshake.d.ts.map +1 -0
- package/dist/ncp/handshake.js +80 -0
- package/dist/ncp/handshake.js.map +1 -0
- package/dist/ncp/index.cjs +188 -0
- package/dist/ncp/index.cjs.map +1 -0
- package/dist/ncp/index.d.cts +6 -0
- package/dist/ncp/index.d.ts +11 -0
- package/dist/ncp/index.d.ts.map +1 -0
- package/dist/ncp/index.js +13 -0
- package/dist/ncp/index.js.map +1 -0
- package/dist/ncp/ncp-error-codes.d.ts +22 -0
- package/dist/ncp/ncp-error-codes.d.ts.map +1 -0
- package/dist/ncp/ncp-error-codes.js +32 -0
- package/dist/ncp/ncp-error-codes.js.map +1 -0
- package/dist/ncp/ncp-patch-format.d.ts +7 -0
- package/dist/ncp/ncp-patch-format.d.ts.map +1 -0
- package/dist/ncp/ncp-patch-format.js +13 -0
- package/dist/ncp/ncp-patch-format.js.map +1 -0
- package/dist/ncp/registry.d.ts +3 -0
- package/dist/ncp/registry.d.ts.map +1 -0
- package/dist/ncp/registry.js +12 -0
- package/dist/ncp/registry.js.map +1 -0
- package/dist/ncp/stream-manager.d.ts +57 -0
- package/dist/ncp/stream-manager.d.ts.map +1 -0
- package/dist/ncp/stream-manager.js +163 -0
- package/dist/ncp/stream-manager.js.map +1 -0
- package/dist/ndp/frames.d.ts +56 -0
- package/dist/ndp/frames.d.ts.map +1 -0
- package/dist/ndp/frames.js +87 -0
- package/dist/ndp/frames.js.map +1 -0
- package/dist/ndp/index.cjs +252 -0
- package/dist/ndp/index.cjs.map +1 -0
- package/dist/ndp/index.d.cts +86 -0
- package/dist/ndp/index.d.ts +5 -0
- package/dist/ndp/index.d.ts.map +1 -0
- package/dist/ndp/index.js +7 -0
- package/dist/ndp/index.js.map +1 -0
- package/dist/ndp/ndp-registry.d.ts +11 -0
- package/dist/ndp/ndp-registry.d.ts.map +1 -0
- package/dist/ndp/ndp-registry.js +79 -0
- package/dist/ndp/ndp-registry.js.map +1 -0
- package/dist/ndp/registry.d.ts +3 -0
- package/dist/ndp/registry.d.ts.map +1 -0
- package/dist/ndp/registry.js +10 -0
- package/dist/ndp/registry.js.map +1 -0
- package/dist/ndp/validator.d.ts +18 -0
- package/dist/ndp/validator.d.ts.map +1 -0
- package/dist/ndp/validator.js +48 -0
- package/dist/ndp/validator.js.map +1 -0
- package/dist/nip/frames.d.ts +44 -0
- package/dist/nip/frames.d.ts.map +1 -0
- package/dist/nip/frames.js +81 -0
- package/dist/nip/frames.js.map +1 -0
- package/dist/nip/identity.d.ts +18 -0
- package/dist/nip/identity.d.ts.map +1 -0
- package/dist/nip/identity.js +94 -0
- package/dist/nip/identity.js.map +1 -0
- package/dist/nip/index.cjs +214 -0
- package/dist/nip/index.cjs.map +1 -0
- package/dist/nip/index.d.cts +65 -0
- package/dist/nip/index.d.ts +4 -0
- package/dist/nip/index.d.ts.map +1 -0
- package/dist/nip/index.js +6 -0
- package/dist/nip/index.js.map +1 -0
- package/dist/nip/registry.d.ts +3 -0
- package/dist/nip/registry.d.ts.map +1 -0
- package/dist/nip/registry.js +10 -0
- package/dist/nip/registry.js.map +1 -0
- package/dist/nop/client.d.ts +34 -0
- package/dist/nop/client.d.ts.map +1 -0
- package/dist/nop/client.js +90 -0
- package/dist/nop/client.js.map +1 -0
- package/dist/nop/frames.d.ts +65 -0
- package/dist/nop/frames.d.ts.map +1 -0
- package/dist/nop/frames.js +148 -0
- package/dist/nop/frames.js.map +1 -0
- package/dist/nop/index.cjs +762 -0
- package/dist/nop/index.cjs.map +1 -0
- package/dist/nop/index.d.cts +155 -0
- package/dist/nop/index.d.ts +5 -0
- package/dist/nop/index.d.ts.map +1 -0
- package/dist/nop/index.js +7 -0
- package/dist/nop/index.js.map +1 -0
- package/dist/nop/models.d.ts +58 -0
- package/dist/nop/models.d.ts.map +1 -0
- package/dist/nop/models.js +50 -0
- package/dist/nop/models.js.map +1 -0
- package/dist/nop/nop-types.d.ts +136 -0
- package/dist/nop/nop-types.d.ts.map +1 -0
- package/dist/nop/nop-types.js +44 -0
- package/dist/nop/nop-types.js.map +1 -0
- package/dist/nop/registry.d.ts +3 -0
- package/dist/nop/registry.d.ts.map +1 -0
- package/dist/nop/registry.js +11 -0
- package/dist/nop/registry.js.map +1 -0
- package/dist/nwp/client.d.ts +22 -0
- package/dist/nwp/client.d.ts.map +1 -0
- package/dist/nwp/client.js +101 -0
- package/dist/nwp/client.js.map +1 -0
- package/dist/nwp/frames.d.ts +46 -0
- package/dist/nwp/frames.d.ts.map +1 -0
- package/dist/nwp/frames.js +81 -0
- package/dist/nwp/frames.js.map +1 -0
- package/dist/nwp/index.cjs +658 -0
- package/dist/nwp/index.cjs.map +1 -0
- package/dist/nwp/index.d.cts +65 -0
- package/dist/nwp/index.d.ts +4 -0
- package/dist/nwp/index.d.ts.map +1 -0
- package/dist/nwp/index.js +6 -0
- package/dist/nwp/index.js.map +1 -0
- package/dist/nwp/registry.d.ts +3 -0
- package/dist/nwp/registry.d.ts.map +1 -0
- package/dist/nwp/registry.js +9 -0
- package/dist/nwp/registry.js.map +1 -0
- package/dist/setup.d.ts +10 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +29 -0
- package/dist/setup.js.map +1 -0
- package/nip-ca-server/Dockerfile +27 -0
- package/nip-ca-server/README.md +45 -0
- package/nip-ca-server/db/001_init.sql +25 -0
- package/nip-ca-server/docker-compose.yml +29 -0
- package/nip-ca-server/package.json +23 -0
- package/nip-ca-server/src/ca.ts +155 -0
- package/nip-ca-server/src/db.ts +104 -0
- package/nip-ca-server/src/index.ts +157 -0
- package/nip-ca-server/tsconfig.json +13 -0
- package/package.json +47 -0
- package/src/core/anchor-cache.ts +129 -0
- package/src/core/cache.ts +93 -0
- package/src/core/canonical-json.ts +50 -0
- package/src/core/codec.ts +158 -0
- package/src/core/codecs/index.ts +5 -0
- package/src/core/codecs/ncp-codec.ts +170 -0
- package/src/core/codecs/tier1-json-codec.ts +33 -0
- package/src/core/codecs/tier2-msgpack-codec.ts +30 -0
- package/src/core/crypto-provider.ts +47 -0
- package/src/core/exceptions.ts +57 -0
- package/src/core/frame-header.ts +282 -0
- package/src/core/frame-registry.ts +91 -0
- package/src/core/frames.ts +183 -0
- package/src/core/index.ts +10 -0
- package/src/core/registry.ts +28 -0
- package/src/core/status-codes.ts +46 -0
- package/src/index.ts +10 -0
- package/src/ncp/frames/anchor-frame.ts +87 -0
- package/src/ncp/frames/caps-frame.ts +59 -0
- package/src/ncp/frames/diff-frame.ts +69 -0
- package/src/ncp/frames/error-frame.ts +26 -0
- package/src/ncp/frames/hello-frame.ts +50 -0
- package/src/ncp/frames/stream-frame.ts +35 -0
- package/src/ncp/frames.ts +199 -0
- package/src/ncp/handshake.ts +95 -0
- package/src/ncp/index.ts +12 -0
- package/src/ncp/ncp-error-codes.ts +34 -0
- package/src/ncp/ncp-patch-format.ts +16 -0
- package/src/ncp/registry.ts +14 -0
- package/src/ncp/stream-manager.ts +212 -0
- package/src/ndp/frames.ts +124 -0
- package/src/ndp/index.ts +7 -0
- package/src/ndp/ndp-registry.ts +82 -0
- package/src/ndp/registry.ts +12 -0
- package/src/ndp/validator.ts +64 -0
- package/src/nip/frames.ts +106 -0
- package/src/nip/identity.ts +113 -0
- package/src/nip/index.ts +6 -0
- package/src/nip/registry.ts +12 -0
- package/src/nop/client.ts +103 -0
- package/src/nop/frames.ts +181 -0
- package/src/nop/index.ts +7 -0
- package/src/nop/models.ts +79 -0
- package/src/nop/nop-types.ts +208 -0
- package/src/nop/registry.ts +13 -0
- package/src/nwp/client.ts +114 -0
- package/src/nwp/frames.ts +116 -0
- package/src/nwp/index.ts +6 -0
- package/src/nwp/registry.ts +11 -0
- package/src/setup.ts +32 -0
- package/tests/core/anchor-cache.test.ts +242 -0
- package/tests/core/codec.test.ts +205 -0
- package/tests/core/frame-registry.test.ts +46 -0
- package/tests/core.test.ts +327 -0
- package/tests/ncp/diff-binary-bitset.test.ts +107 -0
- package/tests/ncp/e2e-enc-reject.test.ts +93 -0
- package/tests/ncp/err-error-frame.test.ts +152 -0
- package/tests/ncp/frames.test.ts +359 -0
- package/tests/ncp/framing.test.ts +233 -0
- package/tests/ncp/hello-frame.test.ts +122 -0
- package/tests/ncp/inline-anchor.test.ts +88 -0
- package/tests/ncp/security.test.ts +184 -0
- package/tests/ncp/stream-window.test.ts +167 -0
- package/tests/ncp/stream.test.ts +242 -0
- package/tests/ncp/version-negotiation.test.ts +123 -0
- package/tests/ndp.test.ts +271 -0
- package/tests/nip.test.ts +184 -0
- package/tests/nop.test.ts +344 -0
- package/tests/nwp.test.ts +237 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +20 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
|
|
3
|
+
//
|
|
4
|
+
// NCP Test Cases — 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
|
+
});
|