@labacacia/nps-sdk 1.0.0-alpha.5 → 1.0.0-alpha.7
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/CHANGELOG.cn.md +29 -5
- package/CHANGELOG.md +29 -5
- package/LICENSE +0 -0
- package/NOTICE +0 -0
- package/README.cn.md +8 -13
- package/README.md +8 -13
- package/dist/nip/index.d.ts +1 -0
- package/dist/nip/index.d.ts.map +1 -1
- package/dist/nip/index.js +2 -0
- package/dist/nip/index.js.map +1 -1
- package/dist/nip/reputation-client.d.ts +116 -0
- package/dist/nip/reputation-client.d.ts.map +1 -0
- package/dist/nip/reputation-client.js +261 -0
- package/dist/nip/reputation-client.js.map +1 -0
- package/dist/nip/x509/oids.d.ts +9 -10
- package/dist/nip/x509/oids.d.ts.map +1 -1
- package/dist/nip/x509/oids.js +3 -4
- package/dist/nip/x509/oids.js.map +1 -1
- package/dist/nwp/anchor-client.d.ts +109 -0
- package/dist/nwp/anchor-client.d.ts.map +1 -0
- package/dist/nwp/anchor-client.js +279 -0
- package/dist/nwp/anchor-client.js.map +1 -0
- package/dist/nwp/index.d.ts +1 -1
- package/dist/nwp/index.d.ts.map +1 -1
- package/dist/nwp/index.js +1 -1
- package/dist/nwp/index.js.map +1 -1
- package/doc/nps-sdk.core.cn.md +0 -0
- package/doc/nps-sdk.core.md +0 -0
- package/doc/nps-sdk.ncp.cn.md +0 -0
- package/doc/nps-sdk.ncp.md +0 -0
- package/doc/nps-sdk.ndp.cn.md +0 -0
- package/doc/nps-sdk.ndp.md +0 -0
- package/doc/nps-sdk.nop.cn.md +0 -0
- package/doc/nps-sdk.nop.md +0 -0
- package/doc/overview.cn.md +0 -0
- package/doc/overview.md +0 -0
- package/package.json +12 -1
- package/CONTRIBUTING.cn.md +0 -35
- package/CONTRIBUTING.md +0 -35
- package/dist/nwp/error-codes.d.ts +0 -42
- package/dist/nwp/error-codes.d.ts.map +0 -1
- package/dist/nwp/error-codes.js +0 -53
- package/dist/nwp/error-codes.js.map +0 -1
- package/nip-ca-server/Dockerfile +0 -27
- package/nip-ca-server/README.md +0 -45
- package/nip-ca-server/db/001_init.sql +0 -25
- package/nip-ca-server/docker-compose.yml +0 -29
- package/nip-ca-server/package.json +0 -23
- package/nip-ca-server/src/ca.ts +0 -155
- package/nip-ca-server/src/db.ts +0 -104
- package/nip-ca-server/src/index.ts +0 -157
- package/nip-ca-server/tsconfig.json +0 -13
- package/src/core/anchor-cache.ts +0 -129
- package/src/core/cache.ts +0 -93
- package/src/core/canonical-json.ts +0 -50
- package/src/core/codec.ts +0 -158
- package/src/core/codecs/index.ts +0 -5
- package/src/core/codecs/ncp-codec.ts +0 -170
- package/src/core/codecs/tier1-json-codec.ts +0 -33
- package/src/core/codecs/tier2-msgpack-codec.ts +0 -30
- package/src/core/crypto-provider.ts +0 -47
- package/src/core/exceptions.ts +0 -57
- package/src/core/frame-header.ts +0 -282
- package/src/core/frame-registry.ts +0 -91
- package/src/core/frames.ts +0 -184
- package/src/core/index.ts +0 -42
- package/src/core/registry.ts +0 -28
- package/src/core/status-codes.ts +0 -47
- package/src/index.ts +0 -10
- package/src/ncp/frames/anchor-frame.ts +0 -87
- package/src/ncp/frames/caps-frame.ts +0 -59
- package/src/ncp/frames/diff-frame.ts +0 -69
- package/src/ncp/frames/error-frame.ts +0 -26
- package/src/ncp/frames/hello-frame.ts +0 -50
- package/src/ncp/frames/stream-frame.ts +0 -35
- package/src/ncp/frames.ts +0 -251
- package/src/ncp/handshake.ts +0 -95
- package/src/ncp/index.ts +0 -13
- package/src/ncp/ncp-error-codes.ts +0 -36
- package/src/ncp/ncp-patch-format.ts +0 -16
- package/src/ncp/preamble.ts +0 -79
- package/src/ncp/registry.ts +0 -15
- package/src/ncp/stream-manager.ts +0 -212
- package/src/ndp/dns-txt.ts +0 -86
- package/src/ndp/frames.ts +0 -124
- package/src/ndp/index.ts +0 -8
- package/src/ndp/ndp-registry.ts +0 -116
- package/src/ndp/registry.ts +0 -12
- package/src/ndp/validator.ts +0 -64
- package/src/nip/acme/client.ts +0 -185
- package/src/nip/acme/index.ts +0 -8
- package/src/nip/acme/jws.ts +0 -109
- package/src/nip/acme/messages.ts +0 -85
- package/src/nip/acme/server.ts +0 -480
- package/src/nip/acme/wire.ts +0 -24
- package/src/nip/assurance-level.ts +0 -40
- package/src/nip/cert-format.ts +0 -9
- package/src/nip/error-codes.ts +0 -38
- package/src/nip/frames.ts +0 -138
- package/src/nip/identity.ts +0 -113
- package/src/nip/index.ts +0 -14
- package/src/nip/registry.ts +0 -12
- package/src/nip/verifier.ts +0 -122
- package/src/nip/x509/builder.ts +0 -91
- package/src/nip/x509/index.ts +0 -6
- package/src/nip/x509/oids.ts +0 -28
- package/src/nip/x509/verifier.ts +0 -214
- package/src/nop/client.ts +0 -103
- package/src/nop/frames.ts +0 -181
- package/src/nop/index.ts +0 -7
- package/src/nop/models.ts +0 -79
- package/src/nop/nop-types.ts +0 -208
- package/src/nop/registry.ts +0 -13
- package/src/nwp/client.ts +0 -114
- package/src/nwp/error-codes.ts +0 -62
- package/src/nwp/frames.ts +0 -116
- package/src/nwp/index.ts +0 -7
- package/src/nwp/registry.ts +0 -11
- package/src/setup.ts +0 -32
- package/tests/_rfc0002-keys.ts +0 -57
- package/tests/core/anchor-cache.test.ts +0 -242
- package/tests/core/codec.test.ts +0 -205
- package/tests/core/frame-registry.test.ts +0 -46
- package/tests/core.test.ts +0 -327
- package/tests/ncp/diff-binary-bitset.test.ts +0 -107
- package/tests/ncp/e2e-enc-reject.test.ts +0 -93
- package/tests/ncp/err-error-frame.test.ts +0 -152
- package/tests/ncp/frames.test.ts +0 -359
- package/tests/ncp/framing.test.ts +0 -233
- package/tests/ncp/hello-frame.test.ts +0 -122
- package/tests/ncp/inline-anchor.test.ts +0 -88
- package/tests/ncp/preamble.test.ts +0 -93
- package/tests/ncp/security.test.ts +0 -184
- package/tests/ncp/stream-window.test.ts +0 -167
- package/tests/ncp/stream.test.ts +0 -242
- package/tests/ncp/version-negotiation.test.ts +0 -123
- package/tests/ndp.test.ts +0 -377
- package/tests/nip-acme-agent01.test.ts +0 -192
- package/tests/nip-x509.test.ts +0 -280
- package/tests/nip.test.ts +0 -184
- package/tests/nop.test.ts +0 -344
- package/tests/nwp.test.ts +0 -237
- package/tsconfig.json +0 -20
- package/tsup.config.ts +0 -20
- package/vitest.config.ts +0 -10
package/src/ncp/handshake.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
|
|
3
|
-
//
|
|
4
|
-
// Handshake — Version negotiation and encoding negotiation
|
|
5
|
-
// NPS-1 §2.6
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Parse a "major.minor" (optionally "major.minor.patch") version string into a
|
|
9
|
-
* tuple of numeric components. Invalid parts become NaN which makes subsequent
|
|
10
|
-
* comparisons return false in both directions (safe failure).
|
|
11
|
-
*/
|
|
12
|
-
function parseVersion(v: string): number[] {
|
|
13
|
-
return v.split(".").map((p) => Number.parseInt(p, 10));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Numeric component-wise comparison of two version strings.
|
|
18
|
-
* Returns negative if a < b, zero if equal, positive if a > b.
|
|
19
|
-
* Avoids the lexicographic pitfall where "0.9" > "0.10".
|
|
20
|
-
*/
|
|
21
|
-
function compareVersions(a: string, b: string): number {
|
|
22
|
-
const partsA = parseVersion(a);
|
|
23
|
-
const partsB = parseVersion(b);
|
|
24
|
-
const len = Math.max(partsA.length, partsB.length);
|
|
25
|
-
for (let i = 0; i < len; i += 1) {
|
|
26
|
-
const x = partsA[i] ?? 0;
|
|
27
|
-
const y = partsB[i] ?? 0;
|
|
28
|
-
if (x !== y) return x - y;
|
|
29
|
-
}
|
|
30
|
-
return 0;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Negotiate the session NPS version between client and server.
|
|
35
|
-
*
|
|
36
|
-
* Session version = numeric min of client.nps_version and server.nps_version
|
|
37
|
-
* (component-wise — "0.9" < "0.10" < "1.0").
|
|
38
|
-
* If the effective client minimum (min_version ?? nps_version) > server.nps_version,
|
|
39
|
-
* the versions are incompatible.
|
|
40
|
-
*
|
|
41
|
-
* Spec: NPS-1 §2.6
|
|
42
|
-
*/
|
|
43
|
-
export function negotiateVersion(
|
|
44
|
-
client: { nps_version: string; min_version?: string },
|
|
45
|
-
server: { nps_version: string },
|
|
46
|
-
): { session_version: string; compatible: boolean; error_code?: string } {
|
|
47
|
-
const clientMin = client.min_version ?? client.nps_version;
|
|
48
|
-
const serverVersion = server.nps_version;
|
|
49
|
-
|
|
50
|
-
if (compareVersions(clientMin, serverVersion) > 0) {
|
|
51
|
-
return {
|
|
52
|
-
session_version: serverVersion,
|
|
53
|
-
compatible: false,
|
|
54
|
-
error_code: "NCP-VERSION-INCOMPATIBLE",
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Session version = component-wise min of client.nps_version and server.nps_version
|
|
59
|
-
const sessionVersion =
|
|
60
|
-
compareVersions(client.nps_version, serverVersion) <= 0
|
|
61
|
-
? client.nps_version
|
|
62
|
-
: serverVersion;
|
|
63
|
-
|
|
64
|
-
return { session_version: sessionVersion, compatible: true };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Negotiate the encoding between client and server preferred lists.
|
|
69
|
-
*
|
|
70
|
-
* Returns the first mutually supported encoding, preferring "msgpack" over "json".
|
|
71
|
-
* Returns null if there is no intersection.
|
|
72
|
-
*/
|
|
73
|
-
export function negotiateEncoding(
|
|
74
|
-
client: string[],
|
|
75
|
-
server: string[],
|
|
76
|
-
): { encoding: string | null } {
|
|
77
|
-
const serverSet = new Set(server);
|
|
78
|
-
|
|
79
|
-
// Prefer msgpack over json (and over any other encoding)
|
|
80
|
-
if (client.includes("msgpack") && serverSet.has("msgpack")) {
|
|
81
|
-
return { encoding: "msgpack" };
|
|
82
|
-
}
|
|
83
|
-
if (client.includes("json") && serverSet.has("json")) {
|
|
84
|
-
return { encoding: "json" };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Fall back to first intersection in client-preference order
|
|
88
|
-
for (const enc of client) {
|
|
89
|
-
if (serverSet.has(enc)) {
|
|
90
|
-
return { encoding: enc };
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return { encoding: null };
|
|
95
|
-
}
|
package/src/ncp/index.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
|
|
3
|
-
export * from "./frames/anchor-frame.js";
|
|
4
|
-
export * from "./frames/caps-frame.js";
|
|
5
|
-
export * from "./frames/diff-frame.js";
|
|
6
|
-
export * from "./frames/error-frame.js";
|
|
7
|
-
export * from "./frames/hello-frame.js";
|
|
8
|
-
export * from "./frames/stream-frame.js";
|
|
9
|
-
export * from "./ncp-error-codes.js";
|
|
10
|
-
export * from "./ncp-patch-format.js";
|
|
11
|
-
export * from "./handshake.js";
|
|
12
|
-
export * from "./stream-manager.js";
|
|
13
|
-
export * from "./preamble.js";
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
|
|
3
|
-
//
|
|
4
|
-
// NCP Error Codes — All v0.4 protocol error codes
|
|
5
|
-
// NPS-1 §6 + §7.4
|
|
6
|
-
//
|
|
7
|
-
// Implementation-only codes (NCP_FRAME_PARSE_ERROR, NCP_FRAME_INCOMPLETE) cover
|
|
8
|
-
// wire-layer parse failures not in spec §6. See test/ncp_test_results.md spec
|
|
9
|
-
// question 2 for the proposal to register them upstream.
|
|
10
|
-
|
|
11
|
-
export const NCP_ERROR_CODES = {
|
|
12
|
-
// Implementation-only codes (not in spec §6 — see test_results.md spec question 2)
|
|
13
|
-
NCP_FRAME_PARSE_ERROR: "NCP-FRAME-PARSE-ERROR",
|
|
14
|
-
NCP_FRAME_INCOMPLETE: "NCP-FRAME-INCOMPLETE",
|
|
15
|
-
// NPS-RFC-0001 — native-mode preamble
|
|
16
|
-
NCP_PREAMBLE_INVALID: "NCP-PREAMBLE-INVALID",
|
|
17
|
-
// Spec-defined codes
|
|
18
|
-
NCP_FRAME_UNKNOWN_TYPE: "NCP-FRAME-UNKNOWN-TYPE",
|
|
19
|
-
NCP_FRAME_PAYLOAD_TOO_LARGE: "NCP-FRAME-PAYLOAD-TOO-LARGE",
|
|
20
|
-
NCP_FRAME_FLAGS_INVALID: "NCP-FRAME-FLAGS-INVALID",
|
|
21
|
-
NCP_ANCHOR_NOT_FOUND: "NCP-ANCHOR-NOT-FOUND",
|
|
22
|
-
NCP_ANCHOR_SCHEMA_INVALID: "NCP-ANCHOR-SCHEMA-INVALID",
|
|
23
|
-
NCP_ANCHOR_ID_MISMATCH: "NCP-ANCHOR-ID-MISMATCH",
|
|
24
|
-
NCP_ANCHOR_STALE: "NCP-ANCHOR-STALE",
|
|
25
|
-
NCP_STREAM_SEQ_GAP: "NCP-STREAM-SEQ-GAP",
|
|
26
|
-
NCP_STREAM_NOT_FOUND: "NCP-STREAM-NOT-FOUND",
|
|
27
|
-
NCP_STREAM_LIMIT_EXCEEDED: "NCP-STREAM-LIMIT-EXCEEDED",
|
|
28
|
-
NCP_STREAM_WINDOW_OVERFLOW: "NCP-STREAM-WINDOW-OVERFLOW",
|
|
29
|
-
NCP_ENCODING_UNSUPPORTED: "NCP-ENCODING-UNSUPPORTED",
|
|
30
|
-
NCP_DIFF_FORMAT_UNSUPPORTED: "NCP-DIFF-FORMAT-UNSUPPORTED",
|
|
31
|
-
NCP_VERSION_INCOMPATIBLE: "NCP-VERSION-INCOMPATIBLE",
|
|
32
|
-
NCP_ENC_NOT_NEGOTIATED: "NCP-ENC-NOT-NEGOTIATED",
|
|
33
|
-
NCP_ENC_AUTH_FAILED: "NCP-ENC-AUTH-FAILED",
|
|
34
|
-
} as const;
|
|
35
|
-
|
|
36
|
-
export type NcpErrorCode = typeof NCP_ERROR_CODES[keyof typeof NCP_ERROR_CODES];
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Copyright (c) 2026 LabAcacia / INNO LOTUS PTY LTD
|
|
3
|
-
//
|
|
4
|
-
// NCP Patch Format — DiffFrame patch encoding types
|
|
5
|
-
// NPS-1 §4.2
|
|
6
|
-
|
|
7
|
-
export const PATCH_FORMAT = {
|
|
8
|
-
JSON_PATCH: "json_patch",
|
|
9
|
-
BINARY_BITSET: "binary_bitset",
|
|
10
|
-
} as const;
|
|
11
|
-
|
|
12
|
-
export type PatchFormat = typeof PATCH_FORMAT[keyof typeof PATCH_FORMAT];
|
|
13
|
-
|
|
14
|
-
export function isValidPatchFormat(v: unknown): v is PatchFormat {
|
|
15
|
-
return v === PATCH_FORMAT.JSON_PATCH || v === PATCH_FORMAT.BINARY_BITSET;
|
|
16
|
-
}
|
package/src/ncp/preamble.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* NCP native-mode connection preamble — the 8-byte ASCII constant
|
|
6
|
-
* `"NPS/1.0\n"` that every native-mode client MUST emit immediately
|
|
7
|
-
* after the transport handshake and before its first HelloFrame.
|
|
8
|
-
* Defined by NPS-RFC-0001 and NPS-1 NCP §2.6.1.
|
|
9
|
-
*
|
|
10
|
-
* HTTP-mode connections do not use the preamble.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
export const PREAMBLE_LITERAL = "NPS/1.0\n";
|
|
14
|
-
export const PREAMBLE_LENGTH = 8;
|
|
15
|
-
export const PREAMBLE_BYTES: Uint8Array = new TextEncoder().encode(PREAMBLE_LITERAL);
|
|
16
|
-
/** Validation timeout in milliseconds (NPS-RFC-0001 §4.1). */
|
|
17
|
-
export const PREAMBLE_READ_TIMEOUT_MS = 10_000;
|
|
18
|
-
/** Maximum delay before closing after a mismatch, in milliseconds. */
|
|
19
|
-
export const PREAMBLE_CLOSE_DEADLINE_MS = 500;
|
|
20
|
-
|
|
21
|
-
export const PREAMBLE_ERROR_CODE = "NCP-PREAMBLE-INVALID";
|
|
22
|
-
export const PREAMBLE_STATUS_CODE = "NPS-PROTO-PREAMBLE-INVALID";
|
|
23
|
-
|
|
24
|
-
export class NcpPreambleInvalidError extends Error {
|
|
25
|
-
readonly errorCode = PREAMBLE_ERROR_CODE;
|
|
26
|
-
readonly statusCode = PREAMBLE_STATUS_CODE;
|
|
27
|
-
|
|
28
|
-
constructor(reason: string) {
|
|
29
|
-
super(reason);
|
|
30
|
-
this.name = "NcpPreambleInvalidError";
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Returns `true` iff `buf` starts with the 8-byte NPS/1.0 preamble.
|
|
36
|
-
* Safe to call with shorter buffers.
|
|
37
|
-
*/
|
|
38
|
-
export function preambleMatches(buf: Uint8Array): boolean {
|
|
39
|
-
if (buf.length < PREAMBLE_LENGTH) return false;
|
|
40
|
-
for (let i = 0; i < PREAMBLE_LENGTH; i++) {
|
|
41
|
-
if (buf[i] !== PREAMBLE_BYTES[i]) return false;
|
|
42
|
-
}
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Validates a presumed-preamble buffer.
|
|
48
|
-
* Returns `{ valid: true, reason: "" }` on success or `{ valid: false, reason }` on failure.
|
|
49
|
-
*/
|
|
50
|
-
export function tryValidatePreamble(buf: Uint8Array): { valid: boolean; reason: string } {
|
|
51
|
-
if (buf.length < PREAMBLE_LENGTH) {
|
|
52
|
-
return { valid: false, reason: `short read (${buf.length}/${PREAMBLE_LENGTH} bytes); peer is not speaking NCP` };
|
|
53
|
-
}
|
|
54
|
-
if (!preambleMatches(buf)) {
|
|
55
|
-
// "NPS/" = 0x4E 0x50 0x53 0x2F
|
|
56
|
-
const isNps = buf[0] === 0x4e && buf[1] === 0x50 && buf[2] === 0x53 && buf[3] === 0x2f;
|
|
57
|
-
if (isNps) {
|
|
58
|
-
return { valid: false, reason: "future-major-version NPS preamble; close with NPS-PREAMBLE-UNSUPPORTED-VERSION diagnostic" };
|
|
59
|
-
}
|
|
60
|
-
return { valid: false, reason: "preamble mismatch; peer is not speaking NPS/1.x" };
|
|
61
|
-
}
|
|
62
|
-
return { valid: true, reason: "" };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Validates a presumed-preamble buffer, throwing {@link NcpPreambleInvalidError} on mismatch.
|
|
67
|
-
*/
|
|
68
|
-
export function validatePreamble(buf: Uint8Array): void {
|
|
69
|
-
const { valid, reason } = tryValidatePreamble(buf);
|
|
70
|
-
if (!valid) throw new NcpPreambleInvalidError(reason);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Writes the preamble bytes to `writer`.
|
|
75
|
-
* `writer` must expose a `write(buf: Uint8Array): void` method (e.g. Node.js `net.Socket`).
|
|
76
|
-
*/
|
|
77
|
-
export function writePreamble(writer: { write(buf: Uint8Array): void }): void {
|
|
78
|
-
writer.write(PREAMBLE_BYTES);
|
|
79
|
-
}
|
package/src/ncp/registry.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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, HelloFrame, 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.HELLO, HelloFrame);
|
|
14
|
-
registry.register(FrameType.ERROR, ErrorFrame);
|
|
15
|
-
}
|
|
@@ -1,212 +0,0 @@
|
|
|
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
|
-
}
|
package/src/ndp/dns-txt.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
import type { NdpResolveResult } from "./frames.js";
|
|
5
|
-
|
|
6
|
-
export const DNS_TXT_DEFAULT_TTL = 300;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Extract the hostname from an NWP target URI.
|
|
10
|
-
* e.g. "nwp://api.example.com/products" → "api.example.com"
|
|
11
|
-
*/
|
|
12
|
-
export function extractHostFromTarget(target: string): string | undefined {
|
|
13
|
-
if (!target.startsWith("nwp://")) return undefined;
|
|
14
|
-
const rest = target.slice("nwp://".length);
|
|
15
|
-
const slashIdx = rest.indexOf("/");
|
|
16
|
-
const host = slashIdx === -1 ? rest : rest.slice(0, slashIdx);
|
|
17
|
-
return host.length > 0 ? host : undefined;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Parse one TXT record (array of string chunks) into an NdpResolveResult.
|
|
22
|
-
*
|
|
23
|
-
* Expected format (chunks joined with a single space):
|
|
24
|
-
* v=nps1 type=<type> port=<port> nid=<nid> fp=sha256:<fingerprint>
|
|
25
|
-
*
|
|
26
|
-
* Rules:
|
|
27
|
-
* - `v` MUST be present and equal to "nps1"
|
|
28
|
-
* - `nid` MUST be present
|
|
29
|
-
* - `port` defaults to 17433 when absent
|
|
30
|
-
* - `fp` is mapped to certFingerprint
|
|
31
|
-
*/
|
|
32
|
-
export function parseNpsTxtRecord(
|
|
33
|
-
parts: string[],
|
|
34
|
-
host: string,
|
|
35
|
-
): NdpResolveResult | undefined {
|
|
36
|
-
const joined = parts.join(" ");
|
|
37
|
-
const kv = new Map<string, string>();
|
|
38
|
-
|
|
39
|
-
for (const token of joined.split(/\s+/)) {
|
|
40
|
-
const eq = token.indexOf("=");
|
|
41
|
-
if (eq === -1) continue;
|
|
42
|
-
const key = token.slice(0, eq);
|
|
43
|
-
const val = token.slice(eq + 1);
|
|
44
|
-
kv.set(key, val);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (kv.get("v") !== "nps1") return undefined;
|
|
48
|
-
|
|
49
|
-
const nid = kv.get("nid");
|
|
50
|
-
if (!nid) return undefined;
|
|
51
|
-
|
|
52
|
-
const rawPort = kv.get("port");
|
|
53
|
-
const port = rawPort !== undefined ? Number(rawPort) : 17433;
|
|
54
|
-
const fp = kv.get("fp");
|
|
55
|
-
|
|
56
|
-
const result: NdpResolveResult = {
|
|
57
|
-
host,
|
|
58
|
-
port,
|
|
59
|
-
ttl: DNS_TXT_DEFAULT_TTL,
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
if (fp !== undefined) {
|
|
63
|
-
result.certFingerprint = fp;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return result;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Abstraction over DNS TXT lookups.
|
|
71
|
-
* The default implementation uses Node's `dns.promises`; pass a mock in tests.
|
|
72
|
-
*/
|
|
73
|
-
export interface DnsTxtLookup {
|
|
74
|
-
resolveTxt(hostname: string): Promise<string[][]>;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* System DNS TXT resolver backed by Node's built-in `dns.promises`.
|
|
79
|
-
* The dynamic `import()` keeps browser bundles from failing at parse time.
|
|
80
|
-
*/
|
|
81
|
-
export class SystemDnsTxtLookup implements DnsTxtLookup {
|
|
82
|
-
async resolveTxt(hostname: string): Promise<string[][]> {
|
|
83
|
-
const { promises: dns } = await import("node:dns");
|
|
84
|
-
return dns.resolveTxt(hostname);
|
|
85
|
-
}
|
|
86
|
-
}
|
package/src/ndp/frames.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
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
DELETED