@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,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
|
+
});
|