@labacacia/nps-sdk 1.0.0-alpha.1 → 1.0.0-alpha.2
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/.npmrc.publish +1 -0
- package/CHANGELOG.cn.md +39 -0
- package/CHANGELOG.md +39 -0
- package/CONTRIBUTING.cn.md +35 -0
- package/CONTRIBUTING.md +2 -0
- package/README.cn.md +155 -0
- package/README.md +5 -3
- package/dist/core/frames.d.ts +1 -0
- package/dist/core/frames.d.ts.map +1 -1
- package/dist/core/frames.js +1 -0
- package/dist/core/frames.js.map +1 -1
- package/dist/core/index.d.ts +6 -4
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +17 -5
- package/dist/core/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/ncp/frames.d.ts +18 -0
- package/dist/ncp/frames.d.ts.map +1 -1
- package/dist/ncp/frames.js +45 -0
- package/dist/ncp/frames.js.map +1 -1
- package/dist/ncp/registry.d.ts.map +1 -1
- package/dist/ncp/registry.js +2 -1
- package/dist/ncp/registry.js.map +1 -1
- package/doc/nps-sdk.core.cn.md +321 -0
- package/doc/nps-sdk.core.md +326 -0
- package/doc/nps-sdk.ncp.cn.md +270 -0
- package/doc/nps-sdk.ncp.md +276 -0
- package/doc/nps-sdk.ndp.cn.md +267 -0
- package/doc/nps-sdk.ndp.md +273 -0
- package/doc/nps-sdk.nip.cn.md +235 -0
- package/doc/nps-sdk.nip.md +242 -0
- package/doc/nps-sdk.nop.cn.md +329 -0
- package/doc/nps-sdk.nop.md +332 -0
- package/doc/nps-sdk.nwp.cn.md +217 -0
- package/doc/nps-sdk.nwp.md +224 -0
- package/doc/overview.cn.md +149 -0
- package/doc/overview.md +153 -0
- package/package.json +21 -4
- package/src/core/frames.ts +1 -0
- package/src/core/index.ts +37 -5
- package/src/index.ts +1 -1
- package/src/ncp/frames.ts +52 -0
- package/src/ncp/registry.ts +2 -1
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
[English Version](./nps-sdk.ndp.md) | 中文版
|
|
2
|
+
|
|
3
|
+
# `@labacacia/nps-sdk/ndp` — 类与方法参考
|
|
4
|
+
|
|
5
|
+
> 规范:[NPS-4 NDP v0.2](https://github.com/labacacia/NPS-Release/blob/main/spec/NPS-4-NDP.md)
|
|
6
|
+
|
|
7
|
+
NDP 是发现层 —— NPS 对应 DNS 的组件。本模块提供三种 NDP 帧类型、
|
|
8
|
+
带惰性 TTL 过期的线程安全内存注册表,以及 announce 签名校验器。
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 目录
|
|
13
|
+
|
|
14
|
+
- [辅助接口](#辅助接口)
|
|
15
|
+
- [`AnnounceFrame` (0x30)](#announceframe-0x30)
|
|
16
|
+
- [`ResolveFrame` (0x31)](#resolveframe-0x31)
|
|
17
|
+
- [`GraphFrame` (0x32)](#graphframe-0x32)
|
|
18
|
+
- [`InMemoryNdpRegistry`](#inmemoryndpregistry)
|
|
19
|
+
- [`NdpAnnounceValidator`](#ndpannouncevalidator)
|
|
20
|
+
- [`NdpAnnounceResult`](#ndpannounceresult)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 辅助接口
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
interface NdpAddress {
|
|
28
|
+
host: string;
|
|
29
|
+
port: number;
|
|
30
|
+
protocol: string; // "nwp" | "nwp+tls"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface NdpGraphNode {
|
|
34
|
+
nid: string;
|
|
35
|
+
addresses: readonly NdpAddress[];
|
|
36
|
+
capabilities: readonly string[];
|
|
37
|
+
nodeType?: string; // "memory" | "action" | …
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface NdpResolveResult {
|
|
41
|
+
host: string;
|
|
42
|
+
port: number;
|
|
43
|
+
ttl: number; // 秒
|
|
44
|
+
certFingerprint?: string; // "sha256:{hex}"
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## `AnnounceFrame` (0x30)
|
|
51
|
+
|
|
52
|
+
发布节点的物理可达性与 TTL(NPS-4 §3.1)。
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
class AnnounceFrame {
|
|
56
|
+
readonly frameType: FrameType.ANNOUNCE;
|
|
57
|
+
readonly preferredTier: EncodingTier.MSGPACK;
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
public readonly nid: string,
|
|
61
|
+
public readonly addresses: readonly NdpAddress[],
|
|
62
|
+
public readonly capabilities: readonly string[],
|
|
63
|
+
public readonly ttl: number, // 0 = 有序下线
|
|
64
|
+
public readonly timestamp: string, // ISO 8601 UTC
|
|
65
|
+
public readonly signature: string, // "ed25519:{base64}"
|
|
66
|
+
public readonly nodeType?: string,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
unsignedDict(): Record<string, unknown>; // 签名 payload(无 signature)
|
|
70
|
+
toDict(): Record<string, unknown>;
|
|
71
|
+
|
|
72
|
+
static fromDict(data: Record<string, unknown>): AnnounceFrame;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
签名流程:
|
|
77
|
+
|
|
78
|
+
1. 调用 `frame.unsignedDict()` —— 剥离 `signature`。
|
|
79
|
+
2. 用 `NipIdentity.sign(dict)` 以该 NID 自己的私钥签名(与支持其
|
|
80
|
+
`IdentFrame` 的相同密钥)。
|
|
81
|
+
3. `ttl = 0` **必须**在有序下线前发布,以便订阅者清除条目。
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## `ResolveFrame` (0x31)
|
|
86
|
+
|
|
87
|
+
解析 `nwp://` URL 的请求 / 响应信封。
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
class ResolveFrame {
|
|
91
|
+
readonly frameType: FrameType.RESOLVE;
|
|
92
|
+
readonly preferredTier: EncodingTier.MSGPACK;
|
|
93
|
+
|
|
94
|
+
constructor(
|
|
95
|
+
public readonly target: string, // "nwp://api.example.com/products"
|
|
96
|
+
public readonly requesterNid?: string,
|
|
97
|
+
public readonly resolved?: NdpResolveResult, // 响应时填充
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
toDict(): Record<string, unknown>;
|
|
101
|
+
static fromDict(data: Record<string, unknown>): ResolveFrame;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Resolve 流量首选 JSON tier —— 量小且便于人类调试。
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## `GraphFrame` (0x32)
|
|
110
|
+
|
|
111
|
+
注册表之间的拓扑同步。
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
class GraphFrame {
|
|
115
|
+
readonly frameType: FrameType.GRAPH;
|
|
116
|
+
readonly preferredTier: EncodingTier.MSGPACK;
|
|
117
|
+
|
|
118
|
+
constructor(
|
|
119
|
+
public readonly seq: number, // 每个发布者严格单调
|
|
120
|
+
public readonly initialSync: boolean,
|
|
121
|
+
public readonly nodes?: readonly NdpGraphNode[], // 全量快照
|
|
122
|
+
public readonly patch?: readonly Record<string, unknown>[], // RFC 6902 JSON Patch
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
toDict(): Record<string, unknown>;
|
|
126
|
+
static fromDict(data: Record<string, unknown>): GraphFrame;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`seq` 跳号**必须**触发重新同步请求,信号为 `NDP-GRAPH-SEQ-GAP`。
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## `InMemoryNdpRegistry`
|
|
135
|
+
|
|
136
|
+
线程安全、按 TTL 过期的注册表。过期是在每次读取时**惰性**评估 ——
|
|
137
|
+
没有后台定时器。
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
class InMemoryNdpRegistry {
|
|
141
|
+
// 为确定性测试可替换
|
|
142
|
+
clock: () => number;
|
|
143
|
+
|
|
144
|
+
announce(frame: AnnounceFrame): void;
|
|
145
|
+
getByNid(nid: string): AnnounceFrame | undefined;
|
|
146
|
+
resolve(target: string): NdpResolveResult | undefined;
|
|
147
|
+
getAll(): AnnounceFrame[];
|
|
148
|
+
|
|
149
|
+
static nwpTargetMatchesNid(nid: string, target: string): boolean;
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 行为
|
|
154
|
+
|
|
155
|
+
- `announce` 若 `ttl === 0` 立即清除该 NID;否则以绝对过期
|
|
156
|
+
`clock() + ttl*1000` 插入 / 刷新条目。
|
|
157
|
+
- `resolve` 扫描活跃条目,找到第一个覆盖 `target` 的 NID,返回其
|
|
158
|
+
第一个广告地址包装为 `NdpResolveResult`。
|
|
159
|
+
- `getByNid` 精确 NID 查询,按需清理。
|
|
160
|
+
- 测试中覆写 `clock`:`registry.clock = () => 1000_000;`
|
|
161
|
+
|
|
162
|
+
### `nwpTargetMatchesNid(nid, target)` *(静态)*
|
|
163
|
+
|
|
164
|
+
NID ↔ target 覆盖规则:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
NID: urn:nps:node:{authority}:{name}
|
|
168
|
+
Target: nwp://{authority}/{name}[/subpath]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
节点 NID 覆盖某 target 的条件:
|
|
172
|
+
|
|
173
|
+
1. Target scheme 为 `nwp://`。
|
|
174
|
+
2. NID authority 等于 target authority(精确,区分大小写)。
|
|
175
|
+
3. Target path 等于 `{name}` 或以 `{name}/` 开头。
|
|
176
|
+
|
|
177
|
+
输入格式错误时返回 `false` 而非抛异常。
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## `NdpAnnounceValidator`
|
|
182
|
+
|
|
183
|
+
使用已注册的 Ed25519 公钥校验 `AnnounceFrame.signature`。
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
class NdpAnnounceValidator {
|
|
187
|
+
registerPublicKey(nid: string, encodedPubKey: string): void;
|
|
188
|
+
removePublicKey(nid: string): void;
|
|
189
|
+
|
|
190
|
+
readonly knownPublicKeys: ReadonlyMap<string, string>;
|
|
191
|
+
|
|
192
|
+
validate(frame: AnnounceFrame): NdpAnnounceResult;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`validate`(NPS-4 §7.1):
|
|
197
|
+
|
|
198
|
+
1. 在已注册密钥中查找 `frame.nid`。缺失 →
|
|
199
|
+
`NdpAnnounceResult.fail("NDP-ANNOUNCE-NID-MISMATCH", …)`。期望的
|
|
200
|
+
工作流程:先校验广告方的 `IdentFrame`,然后把其 `pubKeyString`
|
|
201
|
+
注册到此处。
|
|
202
|
+
2. 用键排序规范形式从 `frame.unsignedDict()` 重建签名 payload。
|
|
203
|
+
3. 运行 Ed25519 verify。
|
|
204
|
+
4. 成功返回 `NdpAnnounceResult.ok()`;失败返回
|
|
205
|
+
`NdpAnnounceResult.fail("NDP-ANNOUNCE-SIG-INVALID", …)`。
|
|
206
|
+
|
|
207
|
+
编码后的密钥**必须**使用 `NipIdentity.pubKeyString` 产生的
|
|
208
|
+
`ed25519:{hex}` 形式。
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## `NdpAnnounceResult`
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
interface NdpAnnounceResult {
|
|
216
|
+
isValid: boolean;
|
|
217
|
+
errorCode?: string;
|
|
218
|
+
message?: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const NdpAnnounceResult: {
|
|
222
|
+
ok(): NdpAnnounceResult;
|
|
223
|
+
fail(errorCode: string, message: string): NdpAnnounceResult;
|
|
224
|
+
};
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## 端到端示例
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import { NipIdentity } from "@labacacia/nps-sdk/nip";
|
|
233
|
+
import {
|
|
234
|
+
AnnounceFrame, InMemoryNdpRegistry, NdpAnnounceValidator,
|
|
235
|
+
} from "@labacacia/nps-sdk/ndp";
|
|
236
|
+
|
|
237
|
+
const id = NipIdentity.generate();
|
|
238
|
+
const nid = "urn:nps:node:api.example.com:products";
|
|
239
|
+
|
|
240
|
+
// 构造并签名 announce
|
|
241
|
+
const unsigned = new AnnounceFrame(
|
|
242
|
+
nid,
|
|
243
|
+
[{ host: "10.0.0.5", port: 17433, protocol: "nwp+tls" }],
|
|
244
|
+
["nwp:query", "nwp:stream"],
|
|
245
|
+
300,
|
|
246
|
+
new Date().toISOString(),
|
|
247
|
+
"placeholder",
|
|
248
|
+
"memory",
|
|
249
|
+
);
|
|
250
|
+
const signature = id.sign(unsigned.unsignedDict());
|
|
251
|
+
const signed = new AnnounceFrame(
|
|
252
|
+
unsigned.nid, unsigned.addresses, unsigned.capabilities,
|
|
253
|
+
unsigned.ttl, unsigned.timestamp, signature, unsigned.nodeType,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// 校验 + 注册
|
|
257
|
+
const validator = new NdpAnnounceValidator();
|
|
258
|
+
validator.registerPublicKey(nid, id.pubKeyString);
|
|
259
|
+
const result = validator.validate(signed);
|
|
260
|
+
if (!result.isValid) throw new Error(result.errorCode);
|
|
261
|
+
|
|
262
|
+
const registry = new InMemoryNdpRegistry();
|
|
263
|
+
registry.announce(signed);
|
|
264
|
+
|
|
265
|
+
const resolved = registry.resolve("nwp://api.example.com/products/items/42");
|
|
266
|
+
// → { host: "10.0.0.5", port: 17433, ttl: 300 }
|
|
267
|
+
```
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
English | [中文版](./nps-sdk.ndp.cn.md)
|
|
2
|
+
|
|
3
|
+
# `@labacacia/nps-sdk/ndp` — Class and Method Reference
|
|
4
|
+
|
|
5
|
+
> Spec: [NPS-4 NDP v0.2](https://github.com/labacacia/NPS-Release/blob/main/spec/NPS-4-NDP.md)
|
|
6
|
+
|
|
7
|
+
NDP is the discovery layer — the NPS analogue of DNS. This module provides
|
|
8
|
+
the three NDP frame types, a thread-safe in-memory registry with lazy
|
|
9
|
+
TTL eviction, and an announce-signature validator.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of contents
|
|
14
|
+
|
|
15
|
+
- [Supporting interfaces](#supporting-interfaces)
|
|
16
|
+
- [`AnnounceFrame` (0x30)](#announceframe-0x30)
|
|
17
|
+
- [`ResolveFrame` (0x31)](#resolveframe-0x31)
|
|
18
|
+
- [`GraphFrame` (0x32)](#graphframe-0x32)
|
|
19
|
+
- [`InMemoryNdpRegistry`](#inmemoryndpregistry)
|
|
20
|
+
- [`NdpAnnounceValidator`](#ndpannouncevalidator)
|
|
21
|
+
- [`NdpAnnounceResult`](#ndpannounceresult)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Supporting interfaces
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
interface NdpAddress {
|
|
29
|
+
host: string;
|
|
30
|
+
port: number;
|
|
31
|
+
protocol: string; // "nwp" | "nwp+tls"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface NdpGraphNode {
|
|
35
|
+
nid: string;
|
|
36
|
+
addresses: readonly NdpAddress[];
|
|
37
|
+
capabilities: readonly string[];
|
|
38
|
+
nodeType?: string; // "memory" | "action" | …
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface NdpResolveResult {
|
|
42
|
+
host: string;
|
|
43
|
+
port: number;
|
|
44
|
+
ttl: number; // seconds
|
|
45
|
+
certFingerprint?: string; // "sha256:{hex}"
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## `AnnounceFrame` (0x30)
|
|
52
|
+
|
|
53
|
+
Publishes a node's physical reachability and TTL (NPS-4 §3.1).
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
class AnnounceFrame {
|
|
57
|
+
readonly frameType: FrameType.ANNOUNCE;
|
|
58
|
+
readonly preferredTier: EncodingTier.MSGPACK;
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
public readonly nid: string,
|
|
62
|
+
public readonly addresses: readonly NdpAddress[],
|
|
63
|
+
public readonly capabilities: readonly string[],
|
|
64
|
+
public readonly ttl: number, // 0 = orderly shutdown
|
|
65
|
+
public readonly timestamp: string, // ISO 8601 UTC
|
|
66
|
+
public readonly signature: string, // "ed25519:{base64}"
|
|
67
|
+
public readonly nodeType?: string,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
unsignedDict(): Record<string, unknown>; // signing payload (no signature)
|
|
71
|
+
toDict(): Record<string, unknown>;
|
|
72
|
+
|
|
73
|
+
static fromDict(data: Record<string, unknown>): AnnounceFrame;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Signing workflow:
|
|
78
|
+
|
|
79
|
+
1. Call `frame.unsignedDict()` — this strips `signature`.
|
|
80
|
+
2. Sign with `NipIdentity.sign(dict)` using the NID's own private key
|
|
81
|
+
(the same key backing its `IdentFrame`).
|
|
82
|
+
3. Publishing `ttl = 0` MUST be done before orderly shutdown so that
|
|
83
|
+
subscribers evict the entry.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## `ResolveFrame` (0x31)
|
|
88
|
+
|
|
89
|
+
Request / response envelope for resolving an `nwp://` URL.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
class ResolveFrame {
|
|
93
|
+
readonly frameType: FrameType.RESOLVE;
|
|
94
|
+
readonly preferredTier: EncodingTier.MSGPACK;
|
|
95
|
+
|
|
96
|
+
constructor(
|
|
97
|
+
public readonly target: string, // "nwp://api.example.com/products"
|
|
98
|
+
public readonly requesterNid?: string,
|
|
99
|
+
public readonly resolved?: NdpResolveResult, // populated on response
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
toDict(): Record<string, unknown>;
|
|
103
|
+
static fromDict(data: Record<string, unknown>): ResolveFrame;
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
JSON tier is preferred for resolve traffic — it's low-volume and
|
|
108
|
+
human-debugged.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## `GraphFrame` (0x32)
|
|
113
|
+
|
|
114
|
+
Topology sync between registries.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
class GraphFrame {
|
|
118
|
+
readonly frameType: FrameType.GRAPH;
|
|
119
|
+
readonly preferredTier: EncodingTier.MSGPACK;
|
|
120
|
+
|
|
121
|
+
constructor(
|
|
122
|
+
public readonly seq: number, // strictly monotonic per publisher
|
|
123
|
+
public readonly initialSync: boolean,
|
|
124
|
+
public readonly nodes?: readonly NdpGraphNode[], // full snapshot
|
|
125
|
+
public readonly patch?: readonly Record<string, unknown>[], // RFC 6902 JSON Patch
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
toDict(): Record<string, unknown>;
|
|
129
|
+
static fromDict(data: Record<string, unknown>): GraphFrame;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Gaps in `seq` MUST trigger a re-sync request signalled with
|
|
134
|
+
`NDP-GRAPH-SEQ-GAP`.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## `InMemoryNdpRegistry`
|
|
139
|
+
|
|
140
|
+
Thread-safe, TTL-evicting registry. Expiry is evaluated **lazily** on
|
|
141
|
+
every read — there is no background timer.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
class InMemoryNdpRegistry {
|
|
145
|
+
// Replaceable for deterministic tests
|
|
146
|
+
clock: () => number;
|
|
147
|
+
|
|
148
|
+
announce(frame: AnnounceFrame): void;
|
|
149
|
+
getByNid(nid: string): AnnounceFrame | undefined;
|
|
150
|
+
resolve(target: string): NdpResolveResult | undefined;
|
|
151
|
+
getAll(): AnnounceFrame[];
|
|
152
|
+
|
|
153
|
+
static nwpTargetMatchesNid(nid: string, target: string): boolean;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Behaviour
|
|
158
|
+
|
|
159
|
+
- `announce` with `ttl === 0` immediately evicts the NID; otherwise the
|
|
160
|
+
entry is inserted / refreshed with absolute expiry `clock() + ttl*1000`.
|
|
161
|
+
- `resolve` scans live entries for the first NID covering `target` and
|
|
162
|
+
returns its first advertised address wrapped in `NdpResolveResult`.
|
|
163
|
+
- `getByNid` does exact NID lookup with on-demand purge.
|
|
164
|
+
- Override `clock` in tests: `registry.clock = () => 1000_000;`
|
|
165
|
+
|
|
166
|
+
### `nwpTargetMatchesNid(nid, target)` *(static)*
|
|
167
|
+
|
|
168
|
+
The NID ↔ target covering rule:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
NID: urn:nps:node:{authority}:{name}
|
|
172
|
+
Target: nwp://{authority}/{name}[/subpath]
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A node NID covers a target when:
|
|
176
|
+
|
|
177
|
+
1. The target scheme is `nwp://`.
|
|
178
|
+
2. The NID authority equals the target authority (exact, case-sensitive).
|
|
179
|
+
3. The target path equals `{name}` or starts with `{name}/`.
|
|
180
|
+
|
|
181
|
+
Returns `false` for malformed inputs rather than throwing.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## `NdpAnnounceValidator`
|
|
186
|
+
|
|
187
|
+
Verifies an `AnnounceFrame.signature` using a registered Ed25519 public
|
|
188
|
+
key.
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
class NdpAnnounceValidator {
|
|
192
|
+
registerPublicKey(nid: string, encodedPubKey: string): void;
|
|
193
|
+
removePublicKey(nid: string): void;
|
|
194
|
+
|
|
195
|
+
readonly knownPublicKeys: ReadonlyMap<string, string>;
|
|
196
|
+
|
|
197
|
+
validate(frame: AnnounceFrame): NdpAnnounceResult;
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`validate` (NPS-4 §7.1):
|
|
202
|
+
|
|
203
|
+
1. Looks up `frame.nid` in the registered keys. Missing →
|
|
204
|
+
`NdpAnnounceResult.fail("NDP-ANNOUNCE-NID-MISMATCH", …)`. Expected
|
|
205
|
+
workflow: verify the announcer's `IdentFrame` first, then register
|
|
206
|
+
its `pubKeyString` here.
|
|
207
|
+
2. Rebuilds the signing payload from `frame.unsignedDict()` using the
|
|
208
|
+
sorted-keys canonical form.
|
|
209
|
+
3. Runs Ed25519 verify.
|
|
210
|
+
4. Returns `NdpAnnounceResult.ok()` on success, or
|
|
211
|
+
`NdpAnnounceResult.fail("NDP-ANNOUNCE-SIG-INVALID", …)` on failure.
|
|
212
|
+
|
|
213
|
+
The encoded key MUST use the `ed25519:{hex}` form produced by
|
|
214
|
+
`NipIdentity.pubKeyString`.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## `NdpAnnounceResult`
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
interface NdpAnnounceResult {
|
|
222
|
+
isValid: boolean;
|
|
223
|
+
errorCode?: string;
|
|
224
|
+
message?: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const NdpAnnounceResult: {
|
|
228
|
+
ok(): NdpAnnounceResult;
|
|
229
|
+
fail(errorCode: string, message: string): NdpAnnounceResult;
|
|
230
|
+
};
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## End-to-end example
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { NipIdentity } from "@labacacia/nps-sdk/nip";
|
|
239
|
+
import {
|
|
240
|
+
AnnounceFrame, InMemoryNdpRegistry, NdpAnnounceValidator,
|
|
241
|
+
} from "@labacacia/nps-sdk/ndp";
|
|
242
|
+
|
|
243
|
+
const id = NipIdentity.generate();
|
|
244
|
+
const nid = "urn:nps:node:api.example.com:products";
|
|
245
|
+
|
|
246
|
+
// Build & sign the announce
|
|
247
|
+
const unsigned = new AnnounceFrame(
|
|
248
|
+
nid,
|
|
249
|
+
[{ host: "10.0.0.5", port: 17433, protocol: "nwp+tls" }],
|
|
250
|
+
["nwp:query", "nwp:stream"],
|
|
251
|
+
300,
|
|
252
|
+
new Date().toISOString(),
|
|
253
|
+
"placeholder",
|
|
254
|
+
"memory",
|
|
255
|
+
);
|
|
256
|
+
const signature = id.sign(unsigned.unsignedDict());
|
|
257
|
+
const signed = new AnnounceFrame(
|
|
258
|
+
unsigned.nid, unsigned.addresses, unsigned.capabilities,
|
|
259
|
+
unsigned.ttl, unsigned.timestamp, signature, unsigned.nodeType,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Validate + register
|
|
263
|
+
const validator = new NdpAnnounceValidator();
|
|
264
|
+
validator.registerPublicKey(nid, id.pubKeyString);
|
|
265
|
+
const result = validator.validate(signed);
|
|
266
|
+
if (!result.isValid) throw new Error(result.errorCode);
|
|
267
|
+
|
|
268
|
+
const registry = new InMemoryNdpRegistry();
|
|
269
|
+
registry.announce(signed);
|
|
270
|
+
|
|
271
|
+
const resolved = registry.resolve("nwp://api.example.com/products/items/42");
|
|
272
|
+
// → { host: "10.0.0.5", port: 17433, ttl: 300 }
|
|
273
|
+
```
|