@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,14 @@
|
|
|
1
|
+
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { FrameRegistry } from "../core/registry.js";
|
|
5
|
+
import { FrameType } from "../core/frames.js";
|
|
6
|
+
import { AnchorFrame, CapsFrame, DiffFrame, ErrorFrame, StreamFrame } from "./frames.js";
|
|
7
|
+
|
|
8
|
+
export function registerNcpFrames(registry: FrameRegistry): void {
|
|
9
|
+
registry.register(FrameType.ANCHOR, AnchorFrame);
|
|
10
|
+
registry.register(FrameType.DIFF, DiffFrame);
|
|
11
|
+
registry.register(FrameType.STREAM, StreamFrame);
|
|
12
|
+
registry.register(FrameType.CAPS, CapsFrame);
|
|
13
|
+
registry.register(FrameType.ERROR, ErrorFrame);
|
|
14
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
|
|
3
|
+
//
|
|
4
|
+
// StreamManager — Concurrent stream tracking, sequence validation, flow control
|
|
5
|
+
// NPS-1 §4.3, §7.3
|
|
6
|
+
|
|
7
|
+
import { NcpError } from "../core/frame-header.js";
|
|
8
|
+
import { NCP_ERROR_CODES } from "./ncp-error-codes.js";
|
|
9
|
+
import type { StreamFrame } from "./frames/stream-frame.js";
|
|
10
|
+
|
|
11
|
+
interface ActiveStream {
|
|
12
|
+
streamId: string;
|
|
13
|
+
expectedSeq: number;
|
|
14
|
+
chunks: unknown[][];
|
|
15
|
+
completed: boolean;
|
|
16
|
+
errorCode?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface OutgoingStream {
|
|
20
|
+
streamId: string;
|
|
21
|
+
remainingWindow: number | undefined; // undefined = no flow control
|
|
22
|
+
paused: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Manages concurrent StreamFrame streams.
|
|
27
|
+
*
|
|
28
|
+
* - Tracks active streams by stream_id
|
|
29
|
+
* - Validates sequential seq numbers
|
|
30
|
+
* - Enforces max concurrent stream limit (NPS-1 §7.3, default 32)
|
|
31
|
+
* - Detects early termination via error_code
|
|
32
|
+
* - Enforces window-based flow control on outgoing streams (NCP-S-07–11)
|
|
33
|
+
*/
|
|
34
|
+
export class StreamManager {
|
|
35
|
+
private readonly streams = new Map<string, ActiveStream>();
|
|
36
|
+
private readonly outgoing = new Map<string, OutgoingStream>();
|
|
37
|
+
private readonly maxConcurrent: number;
|
|
38
|
+
|
|
39
|
+
constructor(options?: { maxConcurrent?: number }) {
|
|
40
|
+
this.maxConcurrent = options?.maxConcurrent ?? 32;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Receive a StreamFrame chunk.
|
|
45
|
+
*
|
|
46
|
+
* @returns true if stream is complete (is_last=true or error_code set).
|
|
47
|
+
* @throws {NcpError} NCP-STREAM-LIMIT-EXCEEDED if too many concurrent streams.
|
|
48
|
+
* @throws {NcpError} NCP-STREAM-NOT-FOUND if frame.seq > 0 for a stream that was never opened.
|
|
49
|
+
* @throws {NcpError} NPS-CLIENT-CONFLICT if the stream_id was already completed (stream-id reuse; see test_cases NCP-S-04).
|
|
50
|
+
* @throws {NcpError} NCP-STREAM-SEQ-GAP if sequence number is not expected.
|
|
51
|
+
*/
|
|
52
|
+
receive(frame: StreamFrame): boolean {
|
|
53
|
+
let stream = this.streams.get(frame.stream_id);
|
|
54
|
+
|
|
55
|
+
if (!stream) {
|
|
56
|
+
// A stream is opened only by seq=0. Any other seq on an unknown stream_id
|
|
57
|
+
// means the opener was never seen (NCP-S-13: unknown stream_id).
|
|
58
|
+
if (frame.seq !== 0) {
|
|
59
|
+
throw new NcpError(
|
|
60
|
+
NCP_ERROR_CODES.NCP_STREAM_NOT_FOUND,
|
|
61
|
+
`Unknown stream_id ${frame.stream_id} — first frame must have seq=0`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// New stream — check concurrent limit
|
|
66
|
+
if (this.streams.size >= this.maxConcurrent) {
|
|
67
|
+
throw new NcpError(
|
|
68
|
+
NCP_ERROR_CODES.NCP_STREAM_LIMIT_EXCEEDED,
|
|
69
|
+
`Max concurrent streams (${this.maxConcurrent}) exceeded`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
stream = {
|
|
74
|
+
streamId: frame.stream_id,
|
|
75
|
+
expectedSeq: 0,
|
|
76
|
+
chunks: [],
|
|
77
|
+
completed: false,
|
|
78
|
+
};
|
|
79
|
+
this.streams.set(frame.stream_id, stream);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Reject writes to completed streams. Per test_cases NCP-S-04, the spec does
|
|
83
|
+
// not assign a dedicated NCP code for stream-id reuse; interim mapping uses
|
|
84
|
+
// the NPS-level NPS-CLIENT-CONFLICT until a spec-side code is added.
|
|
85
|
+
if (stream.completed) {
|
|
86
|
+
throw new NcpError(
|
|
87
|
+
"NPS-CLIENT-CONFLICT",
|
|
88
|
+
`Stream ${frame.stream_id} is already completed — cannot reuse stream_id`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Sequence validation
|
|
93
|
+
if (frame.seq !== stream.expectedSeq) {
|
|
94
|
+
// Duplicate detection — same seq as last accepted
|
|
95
|
+
if (frame.seq < stream.expectedSeq) {
|
|
96
|
+
// Ignore duplicate (idempotent)
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
throw new NcpError(
|
|
100
|
+
NCP_ERROR_CODES.NCP_STREAM_SEQ_GAP,
|
|
101
|
+
`Expected seq ${stream.expectedSeq}, got ${frame.seq} on stream ${frame.stream_id}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
stream.chunks.push(frame.data);
|
|
106
|
+
stream.expectedSeq = frame.seq + 1;
|
|
107
|
+
|
|
108
|
+
// Early termination via error_code
|
|
109
|
+
if (frame.error_code) {
|
|
110
|
+
stream.completed = true;
|
|
111
|
+
stream.errorCode = frame.error_code;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Normal completion
|
|
116
|
+
if (frame.is_last) {
|
|
117
|
+
stream.completed = true;
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Send a StreamFrame on an outgoing stream, enforcing window-based flow control.
|
|
126
|
+
*
|
|
127
|
+
* - seq=0 with window_size initialises remainingWindow (no decrement for opening frame).
|
|
128
|
+
* - Subsequent sends decrement remainingWindow when flow control is active.
|
|
129
|
+
* - Throws NCP-STREAM-WINDOW-OVERFLOW when remainingWindow === 0.
|
|
130
|
+
*
|
|
131
|
+
* @throws {NcpError} NCP-STREAM-WINDOW-OVERFLOW if window is exhausted.
|
|
132
|
+
*/
|
|
133
|
+
send(frame: StreamFrame): void {
|
|
134
|
+
let out = this.outgoing.get(frame.stream_id);
|
|
135
|
+
|
|
136
|
+
if (!out) {
|
|
137
|
+
out = {
|
|
138
|
+
streamId: frame.stream_id,
|
|
139
|
+
remainingWindow: undefined,
|
|
140
|
+
paused: false,
|
|
141
|
+
};
|
|
142
|
+
this.outgoing.set(frame.stream_id, out);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Opening frame: initialise window from window_size if provided.
|
|
146
|
+
if (frame.seq === 0 && frame.window_size !== undefined) {
|
|
147
|
+
out.remainingWindow = frame.window_size;
|
|
148
|
+
return; // opening frame does not consume a window slot
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Flow control check for subsequent frames.
|
|
152
|
+
if (out.remainingWindow !== undefined) {
|
|
153
|
+
if (out.remainingWindow === 0) {
|
|
154
|
+
throw new NcpError(
|
|
155
|
+
NCP_ERROR_CODES.NCP_STREAM_WINDOW_OVERFLOW,
|
|
156
|
+
`Window exhausted on stream ${frame.stream_id}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
out.remainingWindow -= 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Update the send window for a stream.
|
|
165
|
+
*
|
|
166
|
+
* Called when a reverse-direction StreamFrame arrives with data=[] and window_size set.
|
|
167
|
+
* Replaces remainingWindow with new_size. Sets paused=true when new_size === 0.
|
|
168
|
+
*/
|
|
169
|
+
updateWindow(streamId: string, newSize: number): void {
|
|
170
|
+
let out = this.outgoing.get(streamId);
|
|
171
|
+
if (!out) {
|
|
172
|
+
out = {
|
|
173
|
+
streamId,
|
|
174
|
+
remainingWindow: newSize,
|
|
175
|
+
paused: newSize === 0,
|
|
176
|
+
};
|
|
177
|
+
this.outgoing.set(streamId, out);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
out.remainingWindow = newSize;
|
|
181
|
+
out.paused = newSize === 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Returns true when the outgoing stream is paused (window=0 was received).
|
|
186
|
+
* Resumes (returns false) once a non-zero window update arrives.
|
|
187
|
+
*/
|
|
188
|
+
isPaused(streamId: string): boolean {
|
|
189
|
+
return this.outgoing.get(streamId)?.paused ?? false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Get reassembled data for a completed stream. */
|
|
193
|
+
getData(streamId: string): unknown[] | null {
|
|
194
|
+
const stream = this.streams.get(streamId);
|
|
195
|
+
if (!stream || !stream.completed) return null;
|
|
196
|
+
return stream.chunks.flat();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Get error code if stream terminated with error. */
|
|
200
|
+
getError(streamId: string): string | undefined {
|
|
201
|
+
return this.streams.get(streamId)?.errorCode;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Number of active (non-completed) streams. */
|
|
205
|
+
get activeCount(): number {
|
|
206
|
+
let count = 0;
|
|
207
|
+
for (const s of this.streams.values()) {
|
|
208
|
+
if (!s.completed) count++;
|
|
209
|
+
}
|
|
210
|
+
return count;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { EncodingTier, FrameType } from "../core/frames.js";
|
|
5
|
+
import type { NpsFrame } from "../core/codec.js";
|
|
6
|
+
|
|
7
|
+
export interface NdpAddress {
|
|
8
|
+
host: string;
|
|
9
|
+
port: number;
|
|
10
|
+
protocol: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface NdpGraphNode {
|
|
14
|
+
nid: string;
|
|
15
|
+
addresses: readonly NdpAddress[];
|
|
16
|
+
capabilities: readonly string[];
|
|
17
|
+
nodeType?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface NdpResolveResult {
|
|
21
|
+
host: string;
|
|
22
|
+
port: number;
|
|
23
|
+
ttl: number;
|
|
24
|
+
certFingerprint?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class AnnounceFrame implements NpsFrame {
|
|
28
|
+
readonly frameType = FrameType.ANNOUNCE;
|
|
29
|
+
readonly preferredTier = EncodingTier.MSGPACK;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
public readonly nid: string,
|
|
33
|
+
public readonly addresses: readonly NdpAddress[],
|
|
34
|
+
public readonly capabilities: readonly string[],
|
|
35
|
+
public readonly ttl: number,
|
|
36
|
+
public readonly timestamp: string,
|
|
37
|
+
public readonly signature: string,
|
|
38
|
+
public readonly nodeType?: string,
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
unsignedDict(): Record<string, unknown> {
|
|
42
|
+
return {
|
|
43
|
+
nid: this.nid,
|
|
44
|
+
addresses: this.addresses,
|
|
45
|
+
capabilities: this.capabilities,
|
|
46
|
+
ttl: this.ttl,
|
|
47
|
+
timestamp: this.timestamp,
|
|
48
|
+
node_type: this.nodeType ?? null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
toDict(): Record<string, unknown> {
|
|
53
|
+
return { ...this.unsignedDict(), signature: this.signature };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static fromDict(data: Record<string, unknown>): AnnounceFrame {
|
|
57
|
+
return new AnnounceFrame(
|
|
58
|
+
data["nid"] as string,
|
|
59
|
+
data["addresses"] as NdpAddress[],
|
|
60
|
+
data["capabilities"] as string[],
|
|
61
|
+
data["ttl"] as number,
|
|
62
|
+
data["timestamp"] as string,
|
|
63
|
+
data["signature"] as string,
|
|
64
|
+
(data["node_type"] as string | null) ?? undefined,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class ResolveFrame implements NpsFrame {
|
|
70
|
+
readonly frameType = FrameType.RESOLVE;
|
|
71
|
+
readonly preferredTier = EncodingTier.MSGPACK;
|
|
72
|
+
|
|
73
|
+
constructor(
|
|
74
|
+
public readonly target: string,
|
|
75
|
+
public readonly requesterNid?: string,
|
|
76
|
+
public readonly resolved?: NdpResolveResult,
|
|
77
|
+
) {}
|
|
78
|
+
|
|
79
|
+
toDict(): Record<string, unknown> {
|
|
80
|
+
return {
|
|
81
|
+
target: this.target,
|
|
82
|
+
requester_nid: this.requesterNid ?? null,
|
|
83
|
+
resolved: this.resolved ?? null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static fromDict(data: Record<string, unknown>): ResolveFrame {
|
|
88
|
+
return new ResolveFrame(
|
|
89
|
+
data["target"] as string,
|
|
90
|
+
(data["requester_nid"] as string | null) ?? undefined,
|
|
91
|
+
(data["resolved"] as NdpResolveResult | null) ?? undefined,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class GraphFrame implements NpsFrame {
|
|
97
|
+
readonly frameType = FrameType.GRAPH;
|
|
98
|
+
readonly preferredTier = EncodingTier.MSGPACK;
|
|
99
|
+
|
|
100
|
+
constructor(
|
|
101
|
+
public readonly seq: number,
|
|
102
|
+
public readonly initialSync: boolean,
|
|
103
|
+
public readonly nodes?: readonly NdpGraphNode[],
|
|
104
|
+
public readonly patch?: readonly Record<string, unknown>[],
|
|
105
|
+
) {}
|
|
106
|
+
|
|
107
|
+
toDict(): Record<string, unknown> {
|
|
108
|
+
return {
|
|
109
|
+
seq: this.seq,
|
|
110
|
+
initial_sync: this.initialSync,
|
|
111
|
+
nodes: this.nodes ?? null,
|
|
112
|
+
patch: this.patch ?? null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static fromDict(data: Record<string, unknown>): GraphFrame {
|
|
117
|
+
return new GraphFrame(
|
|
118
|
+
data["seq"] as number,
|
|
119
|
+
data["initial_sync"] as boolean,
|
|
120
|
+
(data["nodes"] as NdpGraphNode[] | null) ?? undefined,
|
|
121
|
+
(data["patch"] as Record<string, unknown>[] | null) ?? undefined,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/ndp/index.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { AnnounceFrame, NdpResolveResult } from "./frames.js";
|
|
5
|
+
|
|
6
|
+
interface RegistryEntry {
|
|
7
|
+
frame: AnnounceFrame;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class InMemoryNdpRegistry {
|
|
12
|
+
private readonly _store = new Map<string, RegistryEntry>();
|
|
13
|
+
|
|
14
|
+
// Replaceable for testing
|
|
15
|
+
clock: () => number = () => Date.now();
|
|
16
|
+
|
|
17
|
+
announce(frame: AnnounceFrame): void {
|
|
18
|
+
const expiresAt = this.clock() + frame.ttl * 1000;
|
|
19
|
+
if (frame.ttl === 0) {
|
|
20
|
+
this._store.delete(frame.nid);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
this._store.set(frame.nid, { frame, expiresAt });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getByNid(nid: string): AnnounceFrame | undefined {
|
|
27
|
+
const entry = this._store.get(nid);
|
|
28
|
+
if (entry === undefined) return undefined;
|
|
29
|
+
if (this.clock() > entry.expiresAt) {
|
|
30
|
+
this._store.delete(nid);
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return entry.frame;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
resolve(target: string): NdpResolveResult | undefined {
|
|
37
|
+
for (const [nid, entry] of this._store) {
|
|
38
|
+
if (this.clock() > entry.expiresAt) { this._store.delete(nid); continue; }
|
|
39
|
+
if (!InMemoryNdpRegistry.nwpTargetMatchesNid(nid, target)) continue;
|
|
40
|
+
const addr = entry.frame.addresses[0];
|
|
41
|
+
if (addr === undefined) continue;
|
|
42
|
+
return { host: addr.host, port: addr.port, ttl: entry.frame.ttl };
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getAll(): AnnounceFrame[] {
|
|
48
|
+
const now = this.clock();
|
|
49
|
+
const result: AnnounceFrame[] = [];
|
|
50
|
+
for (const [nid, entry] of this._store) {
|
|
51
|
+
if (now > entry.expiresAt) { this._store.delete(nid); continue; }
|
|
52
|
+
result.push(entry.frame);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static nwpTargetMatchesNid(nid: string, target: string): boolean {
|
|
58
|
+
// NID: urn:nps:node:{authority}:{path-segment}
|
|
59
|
+
// target: nwp://{authority}/{path}
|
|
60
|
+
const nidParts = nid.split(":");
|
|
61
|
+
if (nidParts.length < 5 || nidParts[0] !== "urn" || nidParts[1] !== "nps" || nidParts[2] !== "node") {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (!target.startsWith("nwp://")) return false;
|
|
65
|
+
|
|
66
|
+
const nidAuthority = nidParts[3]!;
|
|
67
|
+
const nidPath = nidParts[4]!;
|
|
68
|
+
const rest = target.slice("nwp://".length);
|
|
69
|
+
const slashIdx = rest.indexOf("/");
|
|
70
|
+
if (slashIdx === -1) return false;
|
|
71
|
+
|
|
72
|
+
const urlAuthority = rest.slice(0, slashIdx);
|
|
73
|
+
const urlPath = rest.slice(slashIdx + 1); // without leading slash
|
|
74
|
+
|
|
75
|
+
if (urlAuthority !== nidAuthority) return false;
|
|
76
|
+
|
|
77
|
+
// nidPath must be a prefix of urlPath at a segment boundary
|
|
78
|
+
if (urlPath === nidPath) return true;
|
|
79
|
+
if (urlPath.startsWith(nidPath + "/")) return true;
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { FrameRegistry } from "../core/registry.js";
|
|
5
|
+
import { FrameType } from "../core/frames.js";
|
|
6
|
+
import { AnnounceFrame, GraphFrame, ResolveFrame } from "./frames.js";
|
|
7
|
+
|
|
8
|
+
export function registerNdpFrames(registry: FrameRegistry): void {
|
|
9
|
+
registry.register(FrameType.ANNOUNCE, AnnounceFrame);
|
|
10
|
+
registry.register(FrameType.RESOLVE, ResolveFrame);
|
|
11
|
+
registry.register(FrameType.GRAPH, GraphFrame);
|
|
12
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import * as ed25519 from "@noble/ed25519";
|
|
5
|
+
import { sha512 } from "@noble/hashes/sha512";
|
|
6
|
+
import type { AnnounceFrame } from "./frames.js";
|
|
7
|
+
|
|
8
|
+
ed25519.etc.sha512Sync = (...m) => sha512(ed25519.etc.concatBytes(...m));
|
|
9
|
+
|
|
10
|
+
export interface NdpAnnounceResult {
|
|
11
|
+
isValid: boolean;
|
|
12
|
+
errorCode?: string;
|
|
13
|
+
message?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const NdpAnnounceResult = {
|
|
17
|
+
ok: (): NdpAnnounceResult => ({ isValid: true }),
|
|
18
|
+
fail: (errorCode: string, message: string): NdpAnnounceResult => ({ isValid: false, errorCode, message }),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class NdpAnnounceValidator {
|
|
22
|
+
private readonly _keys = new Map<string, string>(); // nid → "ed25519:<hex>"
|
|
23
|
+
|
|
24
|
+
registerPublicKey(nid: string, encodedPubKey: string): void {
|
|
25
|
+
this._keys.set(nid, encodedPubKey);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
removePublicKey(nid: string): void {
|
|
29
|
+
this._keys.delete(nid);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get knownPublicKeys(): ReadonlyMap<string, string> {
|
|
33
|
+
return this._keys;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
validate(frame: AnnounceFrame): NdpAnnounceResult {
|
|
37
|
+
const encoded = this._keys.get(frame.nid);
|
|
38
|
+
if (encoded === undefined) {
|
|
39
|
+
return NdpAnnounceResult.fail("NDP-ANNOUNCE-NID-MISMATCH", `No public key registered for NID: ${frame.nid}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const prefix = "ed25519:";
|
|
44
|
+
const pubHex = encoded.startsWith(prefix) ? encoded.slice(prefix.length) : encoded;
|
|
45
|
+
const pubKey = Buffer.from(pubHex, "hex");
|
|
46
|
+
|
|
47
|
+
const sig = frame.signature;
|
|
48
|
+
if (!sig.startsWith(prefix)) {
|
|
49
|
+
return NdpAnnounceResult.fail("NDP-ANNOUNCE-SIG-INVALID", "Signature must start with 'ed25519:'");
|
|
50
|
+
}
|
|
51
|
+
const sigBytes = Buffer.from(sig.slice(prefix.length), "base64");
|
|
52
|
+
|
|
53
|
+
const unsigned = frame.unsignedDict();
|
|
54
|
+
const canonical = JSON.stringify(unsigned, Object.keys(unsigned).sort());
|
|
55
|
+
const message = new TextEncoder().encode(canonical);
|
|
56
|
+
|
|
57
|
+
const valid = ed25519.verify(sigBytes, message, pubKey);
|
|
58
|
+
if (!valid) return NdpAnnounceResult.fail("NDP-ANNOUNCE-SIG-INVALID", "Ed25519 signature verification failed.");
|
|
59
|
+
return NdpAnnounceResult.ok();
|
|
60
|
+
} catch {
|
|
61
|
+
return NdpAnnounceResult.fail("NDP-ANNOUNCE-SIG-INVALID", "Ed25519 signature verification failed.");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { EncodingTier, FrameType } from "../core/frames.js";
|
|
5
|
+
import type { NpsFrame } from "../core/codec.js";
|
|
6
|
+
|
|
7
|
+
export interface IdentMetadata {
|
|
8
|
+
issuer: string;
|
|
9
|
+
issuedAt: string;
|
|
10
|
+
expiresAt?: string;
|
|
11
|
+
capabilities?: readonly string[];
|
|
12
|
+
scopes?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class IdentFrame implements NpsFrame {
|
|
16
|
+
readonly frameType = FrameType.IDENT;
|
|
17
|
+
readonly preferredTier = EncodingTier.MSGPACK;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
public readonly nid: string,
|
|
21
|
+
public readonly pubKey: string,
|
|
22
|
+
public readonly metadata: IdentMetadata,
|
|
23
|
+
public readonly signature: string,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
unsignedDict(): Record<string, unknown> {
|
|
27
|
+
return {
|
|
28
|
+
nid: this.nid,
|
|
29
|
+
pub_key: this.pubKey,
|
|
30
|
+
metadata: this.metadata,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
toDict(): Record<string, unknown> {
|
|
35
|
+
return { ...this.unsignedDict(), signature: this.signature };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static fromDict(data: Record<string, unknown>): IdentFrame {
|
|
39
|
+
return new IdentFrame(
|
|
40
|
+
data["nid"] as string,
|
|
41
|
+
data["pub_key"] as string,
|
|
42
|
+
data["metadata"] as IdentMetadata,
|
|
43
|
+
data["signature"] as string,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class TrustFrame implements NpsFrame {
|
|
49
|
+
readonly frameType = FrameType.TRUST;
|
|
50
|
+
readonly preferredTier = EncodingTier.MSGPACK;
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
public readonly issuerNid: string,
|
|
54
|
+
public readonly subjectNid: string,
|
|
55
|
+
public readonly scopes: readonly string[],
|
|
56
|
+
public readonly expiresAt: string,
|
|
57
|
+
public readonly signature: string,
|
|
58
|
+
) {}
|
|
59
|
+
|
|
60
|
+
toDict(): Record<string, unknown> {
|
|
61
|
+
return {
|
|
62
|
+
issuer_nid: this.issuerNid,
|
|
63
|
+
subject_nid: this.subjectNid,
|
|
64
|
+
scopes: this.scopes,
|
|
65
|
+
expires_at: this.expiresAt,
|
|
66
|
+
signature: this.signature,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static fromDict(data: Record<string, unknown>): TrustFrame {
|
|
71
|
+
return new TrustFrame(
|
|
72
|
+
data["issuer_nid"] as string,
|
|
73
|
+
data["subject_nid"] as string,
|
|
74
|
+
data["scopes"] as string[],
|
|
75
|
+
data["expires_at"] as string,
|
|
76
|
+
data["signature"] as string,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class RevokeFrame implements NpsFrame {
|
|
82
|
+
readonly frameType = FrameType.REVOKE;
|
|
83
|
+
readonly preferredTier = EncodingTier.MSGPACK;
|
|
84
|
+
|
|
85
|
+
constructor(
|
|
86
|
+
public readonly nid: string,
|
|
87
|
+
public readonly reason?: string,
|
|
88
|
+
public readonly revokedAt?: string,
|
|
89
|
+
) {}
|
|
90
|
+
|
|
91
|
+
toDict(): Record<string, unknown> {
|
|
92
|
+
return {
|
|
93
|
+
nid: this.nid,
|
|
94
|
+
reason: this.reason ?? null,
|
|
95
|
+
revoked_at: this.revokedAt ?? null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static fromDict(data: Record<string, unknown>): RevokeFrame {
|
|
100
|
+
return new RevokeFrame(
|
|
101
|
+
data["nid"] as string,
|
|
102
|
+
(data["reason"] as string | null) ?? undefined,
|
|
103
|
+
(data["revoked_at"] as string | null) ?? undefined,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|