@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.
Files changed (145) hide show
  1. package/CHANGELOG.cn.md +29 -5
  2. package/CHANGELOG.md +29 -5
  3. package/LICENSE +0 -0
  4. package/NOTICE +0 -0
  5. package/README.cn.md +8 -13
  6. package/README.md +8 -13
  7. package/dist/nip/index.d.ts +1 -0
  8. package/dist/nip/index.d.ts.map +1 -1
  9. package/dist/nip/index.js +2 -0
  10. package/dist/nip/index.js.map +1 -1
  11. package/dist/nip/reputation-client.d.ts +116 -0
  12. package/dist/nip/reputation-client.d.ts.map +1 -0
  13. package/dist/nip/reputation-client.js +261 -0
  14. package/dist/nip/reputation-client.js.map +1 -0
  15. package/dist/nip/x509/oids.d.ts +9 -10
  16. package/dist/nip/x509/oids.d.ts.map +1 -1
  17. package/dist/nip/x509/oids.js +3 -4
  18. package/dist/nip/x509/oids.js.map +1 -1
  19. package/dist/nwp/anchor-client.d.ts +109 -0
  20. package/dist/nwp/anchor-client.d.ts.map +1 -0
  21. package/dist/nwp/anchor-client.js +279 -0
  22. package/dist/nwp/anchor-client.js.map +1 -0
  23. package/dist/nwp/index.d.ts +1 -1
  24. package/dist/nwp/index.d.ts.map +1 -1
  25. package/dist/nwp/index.js +1 -1
  26. package/dist/nwp/index.js.map +1 -1
  27. package/doc/nps-sdk.core.cn.md +0 -0
  28. package/doc/nps-sdk.core.md +0 -0
  29. package/doc/nps-sdk.ncp.cn.md +0 -0
  30. package/doc/nps-sdk.ncp.md +0 -0
  31. package/doc/nps-sdk.ndp.cn.md +0 -0
  32. package/doc/nps-sdk.ndp.md +0 -0
  33. package/doc/nps-sdk.nop.cn.md +0 -0
  34. package/doc/nps-sdk.nop.md +0 -0
  35. package/doc/overview.cn.md +0 -0
  36. package/doc/overview.md +0 -0
  37. package/package.json +12 -1
  38. package/CONTRIBUTING.cn.md +0 -35
  39. package/CONTRIBUTING.md +0 -35
  40. package/dist/nwp/error-codes.d.ts +0 -42
  41. package/dist/nwp/error-codes.d.ts.map +0 -1
  42. package/dist/nwp/error-codes.js +0 -53
  43. package/dist/nwp/error-codes.js.map +0 -1
  44. package/nip-ca-server/Dockerfile +0 -27
  45. package/nip-ca-server/README.md +0 -45
  46. package/nip-ca-server/db/001_init.sql +0 -25
  47. package/nip-ca-server/docker-compose.yml +0 -29
  48. package/nip-ca-server/package.json +0 -23
  49. package/nip-ca-server/src/ca.ts +0 -155
  50. package/nip-ca-server/src/db.ts +0 -104
  51. package/nip-ca-server/src/index.ts +0 -157
  52. package/nip-ca-server/tsconfig.json +0 -13
  53. package/src/core/anchor-cache.ts +0 -129
  54. package/src/core/cache.ts +0 -93
  55. package/src/core/canonical-json.ts +0 -50
  56. package/src/core/codec.ts +0 -158
  57. package/src/core/codecs/index.ts +0 -5
  58. package/src/core/codecs/ncp-codec.ts +0 -170
  59. package/src/core/codecs/tier1-json-codec.ts +0 -33
  60. package/src/core/codecs/tier2-msgpack-codec.ts +0 -30
  61. package/src/core/crypto-provider.ts +0 -47
  62. package/src/core/exceptions.ts +0 -57
  63. package/src/core/frame-header.ts +0 -282
  64. package/src/core/frame-registry.ts +0 -91
  65. package/src/core/frames.ts +0 -184
  66. package/src/core/index.ts +0 -42
  67. package/src/core/registry.ts +0 -28
  68. package/src/core/status-codes.ts +0 -47
  69. package/src/index.ts +0 -10
  70. package/src/ncp/frames/anchor-frame.ts +0 -87
  71. package/src/ncp/frames/caps-frame.ts +0 -59
  72. package/src/ncp/frames/diff-frame.ts +0 -69
  73. package/src/ncp/frames/error-frame.ts +0 -26
  74. package/src/ncp/frames/hello-frame.ts +0 -50
  75. package/src/ncp/frames/stream-frame.ts +0 -35
  76. package/src/ncp/frames.ts +0 -251
  77. package/src/ncp/handshake.ts +0 -95
  78. package/src/ncp/index.ts +0 -13
  79. package/src/ncp/ncp-error-codes.ts +0 -36
  80. package/src/ncp/ncp-patch-format.ts +0 -16
  81. package/src/ncp/preamble.ts +0 -79
  82. package/src/ncp/registry.ts +0 -15
  83. package/src/ncp/stream-manager.ts +0 -212
  84. package/src/ndp/dns-txt.ts +0 -86
  85. package/src/ndp/frames.ts +0 -124
  86. package/src/ndp/index.ts +0 -8
  87. package/src/ndp/ndp-registry.ts +0 -116
  88. package/src/ndp/registry.ts +0 -12
  89. package/src/ndp/validator.ts +0 -64
  90. package/src/nip/acme/client.ts +0 -185
  91. package/src/nip/acme/index.ts +0 -8
  92. package/src/nip/acme/jws.ts +0 -109
  93. package/src/nip/acme/messages.ts +0 -85
  94. package/src/nip/acme/server.ts +0 -480
  95. package/src/nip/acme/wire.ts +0 -24
  96. package/src/nip/assurance-level.ts +0 -40
  97. package/src/nip/cert-format.ts +0 -9
  98. package/src/nip/error-codes.ts +0 -38
  99. package/src/nip/frames.ts +0 -138
  100. package/src/nip/identity.ts +0 -113
  101. package/src/nip/index.ts +0 -14
  102. package/src/nip/registry.ts +0 -12
  103. package/src/nip/verifier.ts +0 -122
  104. package/src/nip/x509/builder.ts +0 -91
  105. package/src/nip/x509/index.ts +0 -6
  106. package/src/nip/x509/oids.ts +0 -28
  107. package/src/nip/x509/verifier.ts +0 -214
  108. package/src/nop/client.ts +0 -103
  109. package/src/nop/frames.ts +0 -181
  110. package/src/nop/index.ts +0 -7
  111. package/src/nop/models.ts +0 -79
  112. package/src/nop/nop-types.ts +0 -208
  113. package/src/nop/registry.ts +0 -13
  114. package/src/nwp/client.ts +0 -114
  115. package/src/nwp/error-codes.ts +0 -62
  116. package/src/nwp/frames.ts +0 -116
  117. package/src/nwp/index.ts +0 -7
  118. package/src/nwp/registry.ts +0 -11
  119. package/src/setup.ts +0 -32
  120. package/tests/_rfc0002-keys.ts +0 -57
  121. package/tests/core/anchor-cache.test.ts +0 -242
  122. package/tests/core/codec.test.ts +0 -205
  123. package/tests/core/frame-registry.test.ts +0 -46
  124. package/tests/core.test.ts +0 -327
  125. package/tests/ncp/diff-binary-bitset.test.ts +0 -107
  126. package/tests/ncp/e2e-enc-reject.test.ts +0 -93
  127. package/tests/ncp/err-error-frame.test.ts +0 -152
  128. package/tests/ncp/frames.test.ts +0 -359
  129. package/tests/ncp/framing.test.ts +0 -233
  130. package/tests/ncp/hello-frame.test.ts +0 -122
  131. package/tests/ncp/inline-anchor.test.ts +0 -88
  132. package/tests/ncp/preamble.test.ts +0 -93
  133. package/tests/ncp/security.test.ts +0 -184
  134. package/tests/ncp/stream-window.test.ts +0 -167
  135. package/tests/ncp/stream.test.ts +0 -242
  136. package/tests/ncp/version-negotiation.test.ts +0 -123
  137. package/tests/ndp.test.ts +0 -377
  138. package/tests/nip-acme-agent01.test.ts +0 -192
  139. package/tests/nip-x509.test.ts +0 -280
  140. package/tests/nip.test.ts +0 -184
  141. package/tests/nop.test.ts +0 -344
  142. package/tests/nwp.test.ts +0 -237
  143. package/tsconfig.json +0 -20
  144. package/tsup.config.ts +0 -20
  145. package/vitest.config.ts +0 -10
@@ -1,480 +0,0 @@
1
- // Copyright 2026 INNO LOTUS PTY LTD
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
- /**
5
- * In-process ACME server implementing the `agent-01` challenge for NPS-RFC-0002 §4.4.
6
- *
7
- * Backed by Node's stdlib `http.createServer`. Suitable for tests and reference
8
- * deployments. State is kept in memory.
9
- */
10
-
11
- import * as ed25519 from "@noble/ed25519";
12
- import { sha512 } from "@noble/hashes/sha512";
13
- import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
14
- import { randomBytes } from "node:crypto";
15
- import * as x509 from "@peculiar/x509";
16
-
17
- import { AssuranceLevel } from "../assurance-level.js";
18
- import { ACME_CHALLENGE_FAILED } from "../error-codes.js";
19
- import { issueLeaf } from "../x509/builder.js";
20
- import * as Jws from "./jws.js";
21
- import type {
22
- Authorization, Challenge, ChallengeRespondPayload, Directory,
23
- FinalizePayload, Identifier, NewOrderPayload, Order, ProblemDetail,
24
- } from "./messages.js";
25
- import * as wire from "./wire.js";
26
-
27
- ed25519.etc.sha512Sync = (...m) => sha512(ed25519.etc.concatBytes(...m));
28
- x509.cryptoProvider.set(globalThis.crypto);
29
-
30
- export interface AcmeServerOptions {
31
- caNid: string;
32
- caKeys: CryptoKeyPair; // Web Crypto Ed25519 keypair (for issuing X.509 leaves).
33
- caRootCert: x509.X509Certificate;
34
- certValidityMs: number;
35
- }
36
-
37
- interface OrderState {
38
- id: string;
39
- identifier: Identifier;
40
- status: string;
41
- authzId: string;
42
- finalizeUrl: string;
43
- accountUrl: string;
44
- certificateUrl?: string;
45
- }
46
-
47
- interface AuthzState {
48
- id: string;
49
- identifier: Identifier;
50
- status: string;
51
- challengeIds: string[];
52
- accountUrl: string;
53
- }
54
-
55
- interface ChallengeState {
56
- id: string;
57
- type: string;
58
- status: string;
59
- token: string;
60
- authzId: string;
61
- accountUrl: string;
62
- }
63
-
64
- export class AcmeServer {
65
- private readonly server: Server;
66
- private readonly nonces = new Set<string>();
67
- private readonly accountJwks = new Map<string, Jws.Jwk>();
68
- private readonly orders = new Map<string, OrderState>();
69
- private readonly authzs = new Map<string, AuthzState>();
70
- private readonly challenges = new Map<string, ChallengeState>();
71
- private readonly certs = new Map<string, string>();
72
- private boundPort: number = 0;
73
-
74
- constructor(public readonly options: AcmeServerOptions) {
75
- this.server = createServer((req, res) => this.dispatch(req, res));
76
- }
77
-
78
- async start(): Promise<this> {
79
- await new Promise<void>((resolve) => {
80
- this.server.listen(0, "127.0.0.1", () => resolve());
81
- });
82
- const addr = this.server.address();
83
- this.boundPort = typeof addr === "object" && addr !== null ? addr.port : 0;
84
- return this;
85
- }
86
-
87
- close(): Promise<void> {
88
- return new Promise((resolve) => this.server.close(() => resolve()));
89
- }
90
-
91
- get baseUrl(): string { return `http://127.0.0.1:${this.boundPort}`; }
92
- get directoryUrl(): string { return `${this.baseUrl}/directory`; }
93
-
94
- // ── Routing ──────────────────────────────────────────────────────────────
95
-
96
- private async dispatch(req: IncomingMessage, res: ServerResponse): Promise<void> {
97
- const url = req.url ?? "/";
98
- const method = req.method ?? "GET";
99
- try {
100
- if (method === "GET" && url === "/directory") return this.handleDirectory(res);
101
- if (url === "/new-nonce") return this.handleNewNonce(method, res);
102
- if (method === "POST" && url === "/new-account") return await this.handleNewAccount(req, res);
103
- if (method === "POST" && url === "/new-order") return await this.handleNewOrder(req, res);
104
- if (method === "POST" && url.startsWith("/authz/")) return await this.handleAuthz(req, res, url);
105
- if (method === "POST" && url.startsWith("/chall/")) return await this.handleChallenge(req, res, url);
106
- if (method === "POST" && url.startsWith("/finalize/")) return await this.handleFinalize(req, res, url);
107
- if (method === "POST" && url.startsWith("/cert/")) return await this.handleCert(req, res, url);
108
- if (method === "POST" && url.startsWith("/order/")) return await this.handleOrder(req, res, url);
109
- this.sendProblem(res, 404, "urn:ietf:params:acme:error:malformed", "no such resource");
110
- } catch (e) {
111
- this.sendProblem(res, 500, "urn:ietf:params:acme:error:serverInternal",
112
- (e as Error).message);
113
- }
114
- }
115
-
116
- // ── Endpoint handlers ────────────────────────────────────────────────────
117
-
118
- private handleDirectory(res: ServerResponse): void {
119
- const dir: Directory = {
120
- newNonce: `${this.baseUrl}/new-nonce`,
121
- newAccount: `${this.baseUrl}/new-account`,
122
- newOrder: `${this.baseUrl}/new-order`,
123
- };
124
- this.sendJson(res, 200, dir);
125
- }
126
-
127
- private handleNewNonce(method: string, res: ServerResponse): void {
128
- res.statusCode = method === "HEAD" ? 200 : 204;
129
- res.setHeader("Replay-Nonce", this.mintNonce());
130
- res.setHeader("Cache-Control", "no-store");
131
- res.end();
132
- }
133
-
134
- private async handleNewAccount(req: IncomingMessage, res: ServerResponse): Promise<void> {
135
- const env = await this.readEnvelope(req, res);
136
- if (!env) return;
137
- const header = this.parseHeader(env, res);
138
- if (!header) return;
139
- if (!header.jwk) {
140
- this.sendProblem(res, 400, "urn:ietf:params:acme:error:malformed",
141
- "newAccount must include a 'jwk' member");
142
- return;
143
- }
144
- if (!this.consumeNonce(header.nonce)) {
145
- this.sendProblem(res, 400, "urn:ietf:params:acme:error:badNonce", "invalid nonce");
146
- return;
147
- }
148
- const pub = Jws.publicKeyFromJwk(header.jwk);
149
- if (Jws.verify(env, pub) === null) {
150
- this.sendProblem(res, 400, "urn:ietf:params:acme:error:malformed",
151
- "JWS signature verify failed");
152
- return;
153
- }
154
-
155
- const accountId = `acc-${shortId()}`;
156
- const accountUrl = `${this.baseUrl}/account/${accountId}`;
157
- this.accountJwks.set(accountUrl, header.jwk);
158
-
159
- res.statusCode = 201;
160
- res.setHeader("Content-Type", "application/json");
161
- res.setHeader("Location", accountUrl);
162
- res.setHeader("Replay-Nonce", this.mintNonce());
163
- res.end(JSON.stringify({ status: wire.Status.VALID }));
164
- }
165
-
166
- private async handleNewOrder(req: IncomingMessage, res: ServerResponse): Promise<void> {
167
- const env = await this.readEnvelope(req, res);
168
- if (!env) return;
169
- const header = this.parseHeader(env, res);
170
- if (!header) return;
171
- if (!this.consumeNonce(header.nonce)) {
172
- this.sendProblem(res, 400, "urn:ietf:params:acme:error:badNonce", "invalid nonce"); return;
173
- }
174
- if (!this.verifyAccount(env, header)) {
175
- this.sendProblem(res, 401, "urn:ietf:params:acme:error:accountDoesNotExist",
176
- `unknown kid: ${header.kid ?? "<missing>"}`);
177
- return;
178
- }
179
-
180
- const payload = Jws.decodePayload<NewOrderPayload>(env);
181
- if (!payload || !payload.identifiers?.length) {
182
- this.sendProblem(res, 400, "urn:ietf:params:acme:error:malformed", "missing identifiers");
183
- return;
184
- }
185
- const ident = payload.identifiers[0];
186
- const orderId = `ord-${shortId()}`;
187
- const authzId = `az-${shortId()}`;
188
- const challId = `ch-${shortId()}`;
189
- const token = Jws.b64uEncode(new Uint8Array(randomBytes(32)));
190
-
191
- const orderUrl = `${this.baseUrl}/order/${orderId}`;
192
- const authzUrl = `${this.baseUrl}/authz/${authzId}`;
193
- const challUrl = `${this.baseUrl}/chall/${challId}`;
194
- const finalizeUrl = `${this.baseUrl}/finalize/${orderId}`;
195
-
196
- this.challenges.set(challId, {
197
- id: challId, type: wire.CHALLENGE_AGENT_01, status: wire.Status.PENDING,
198
- token, authzId, accountUrl: header.kid ?? "",
199
- });
200
- this.authzs.set(authzId, {
201
- id: authzId, identifier: ident, status: wire.Status.PENDING,
202
- challengeIds: [challId], accountUrl: header.kid ?? "",
203
- });
204
- this.orders.set(orderId, {
205
- id: orderId, identifier: ident, status: wire.Status.PENDING,
206
- authzId, finalizeUrl, accountUrl: header.kid ?? "",
207
- });
208
-
209
- const order: Order = {
210
- status: wire.Status.PENDING,
211
- identifiers: [ident],
212
- authorizations: [authzUrl],
213
- finalize: finalizeUrl,
214
- };
215
- res.statusCode = 201;
216
- res.setHeader("Content-Type", "application/json");
217
- res.setHeader("Location", orderUrl);
218
- res.setHeader("Replay-Nonce", this.mintNonce());
219
- res.end(JSON.stringify(order));
220
- }
221
-
222
- private async handleAuthz(req: IncomingMessage, res: ServerResponse, url: string): Promise<void> {
223
- const env = await this.readEnvelope(req, res); if (!env) return;
224
- const header = this.parseHeader(env, res); if (!header) return;
225
- if (!this.consumeNonce(header.nonce)) {
226
- this.sendProblem(res, 400, "urn:ietf:params:acme:error:badNonce", "invalid nonce"); return;
227
- }
228
- if (!this.verifyAccount(env, header)) {
229
- this.sendProblem(res, 401, "urn:ietf:params:acme:error:unauthorized", "bad sig"); return;
230
- }
231
- const id = url.replace(/^\/authz\//, "");
232
- const az = this.authzs.get(id);
233
- if (!az) { this.sendProblem(res, 404, "urn:ietf:params:acme:error:malformed", "no authz"); return; }
234
-
235
- const challenges: Challenge[] = az.challengeIds.map((cid) => {
236
- const cs = this.challenges.get(cid)!;
237
- return {
238
- type: cs.type, url: `${this.baseUrl}/chall/${cs.id}`,
239
- status: cs.status, token: cs.token,
240
- };
241
- });
242
- const authz: Authorization = {
243
- status: az.status, identifier: az.identifier, challenges,
244
- };
245
- res.setHeader("Replay-Nonce", this.mintNonce());
246
- this.sendJson(res, 200, authz);
247
- }
248
-
249
- private async handleChallenge(req: IncomingMessage, res: ServerResponse, url: string): Promise<void> {
250
- const env = await this.readEnvelope(req, res); if (!env) return;
251
- const header = this.parseHeader(env, res); if (!header) return;
252
- if (!this.consumeNonce(header.nonce)) {
253
- this.sendProblem(res, 400, "urn:ietf:params:acme:error:badNonce", "invalid nonce"); return;
254
- }
255
- const accountJwk = this.accountJwks.get(header.kid ?? "");
256
- if (!accountJwk) {
257
- this.sendProblem(res, 401, "urn:ietf:params:acme:error:accountDoesNotExist", "unknown kid");
258
- return;
259
- }
260
- const accountPub = Jws.publicKeyFromJwk(accountJwk);
261
- if (Jws.verify(env, accountPub) === null) {
262
- this.sendProblem(res, 400, "urn:ietf:params:acme:error:malformed", "JWS sig fail"); return;
263
- }
264
-
265
- const id = url.replace(/^\/chall\//, "");
266
- const ch = this.challenges.get(id);
267
- if (!ch) { this.sendProblem(res, 404, "urn:ietf:params:acme:error:malformed", "no chall"); return; }
268
-
269
- const payload = Jws.decodePayload<ChallengeRespondPayload>(env);
270
- if (!payload?.agent_signature) {
271
- ch.status = wire.Status.INVALID;
272
- this.sendProblem(res, 400, ACME_CHALLENGE_FAILED,
273
- "missing agent_signature in challenge response");
274
- return;
275
- }
276
- try {
277
- const sigBytes = Jws.b64uDecode(payload.agent_signature);
278
- const tokenBytes = new TextEncoder().encode(ch.token);
279
- if (!ed25519.verify(sigBytes, tokenBytes, accountPub)) {
280
- ch.status = wire.Status.INVALID;
281
- this.sendProblem(res, 400, ACME_CHALLENGE_FAILED,
282
- "agent-01 signature did not verify");
283
- return;
284
- }
285
- } catch (e) {
286
- ch.status = wire.Status.INVALID;
287
- this.sendProblem(res, 400, ACME_CHALLENGE_FAILED,
288
- `agent-01 verification error: ${(e as Error).message}`);
289
- return;
290
- }
291
-
292
- ch.status = wire.Status.VALID;
293
- const az = this.authzs.get(ch.authzId);
294
- if (az) az.status = wire.Status.VALID;
295
- for (const o of this.orders.values()) {
296
- if (o.authzId === ch.authzId) o.status = wire.Status.READY;
297
- }
298
-
299
- res.setHeader("Replay-Nonce", this.mintNonce());
300
- this.sendJson(res, 200, {
301
- type: ch.type, url: `${this.baseUrl}/chall/${ch.id}`,
302
- status: ch.status, token: ch.token,
303
- } as Challenge);
304
- }
305
-
306
- private async handleFinalize(req: IncomingMessage, res: ServerResponse, url: string): Promise<void> {
307
- const env = await this.readEnvelope(req, res); if (!env) return;
308
- const header = this.parseHeader(env, res); if (!header) return;
309
- if (!this.consumeNonce(header.nonce)) {
310
- this.sendProblem(res, 400, "urn:ietf:params:acme:error:badNonce", "invalid nonce"); return;
311
- }
312
- if (!this.verifyAccount(env, header)) {
313
- this.sendProblem(res, 401, "urn:ietf:params:acme:error:unauthorized", "bad sig"); return;
314
- }
315
- const orderId = url.replace(/^\/finalize\//, "");
316
- const os = this.orders.get(orderId);
317
- if (!os) { this.sendProblem(res, 404, "urn:ietf:params:acme:error:malformed", "no order"); return; }
318
- if (os.status !== wire.Status.READY) {
319
- this.sendProblem(res, 403, "urn:ietf:params:acme:error:orderNotReady",
320
- `order is in state '${os.status}', not 'ready'`);
321
- return;
322
- }
323
- const fp = Jws.decodePayload<FinalizePayload>(env);
324
- if (!fp?.csr) {
325
- this.sendProblem(res, 400, "urn:ietf:params:acme:error:malformed", "missing csr"); return;
326
- }
327
-
328
- try {
329
- const csrDer = Jws.b64uDecode(fp.csr);
330
- const csr = new x509.Pkcs10CertificateRequest(csrDer.buffer as ArrayBuffer);
331
- const subjectCn = (() => {
332
- for (const rdn of csr.subject.split(",")) {
333
- const t = rdn.trim();
334
- if (t.startsWith("CN=")) return t.slice(3).replace(/\\([",+;<>\\])/g, "$1");
335
- }
336
- return null as string | null;
337
- })();
338
- if (subjectCn !== os.identifier.value) {
339
- this.sendProblem(res, 400, "NIP-CERT-SUBJECT-NID-MISMATCH",
340
- `CSR subject CN '${subjectCn ?? ""}' does not match order identifier '${os.identifier.value}'`);
341
- return;
342
- }
343
- const subjectPub = await csr.publicKey.export();
344
- const now = new Date();
345
- const leaf = await issueLeaf({
346
- subjectNid: os.identifier.value,
347
- subjectPublicKey: subjectPub,
348
- caKeys: this.options.caKeys,
349
- issuerNid: this.options.caNid,
350
- role: "agent",
351
- assuranceLevel: AssuranceLevel.ANONYMOUS,
352
- notBefore: new Date(now.getTime() - 60_000),
353
- notAfter: new Date(now.getTime() + this.options.certValidityMs),
354
- serialNumber: randomHexSerial(),
355
- });
356
- const certId = `crt-${shortId()}`;
357
- const certUrl = `${this.baseUrl}/cert/${certId}`;
358
- const pem = leaf.toString("pem") + this.options.caRootCert.toString("pem");
359
- this.certs.set(certId, pem);
360
- os.status = wire.Status.VALID;
361
- os.certificateUrl = certUrl;
362
- } catch (e) {
363
- this.sendProblem(res, 400, "urn:ietf:params:acme:error:badCSR",
364
- `CSR processing failed: ${(e as Error).message}`);
365
- return;
366
- }
367
-
368
- const authzUrl = `${this.baseUrl}/authz/${os.authzId}`;
369
- res.setHeader("Replay-Nonce", this.mintNonce());
370
- this.sendJson(res, 200, {
371
- status: os.status, identifiers: [os.identifier],
372
- authorizations: [authzUrl], finalize: os.finalizeUrl,
373
- certificate: os.certificateUrl,
374
- } as Order);
375
- }
376
-
377
- private async handleCert(req: IncomingMessage, res: ServerResponse, url: string): Promise<void> {
378
- const env = await this.readEnvelope(req, res); if (!env) return;
379
- const header = this.parseHeader(env, res); if (!header) return;
380
- if (!this.consumeNonce(header.nonce)) {
381
- this.sendProblem(res, 400, "urn:ietf:params:acme:error:badNonce", "invalid nonce"); return;
382
- }
383
- if (!this.verifyAccount(env, header)) {
384
- this.sendProblem(res, 401, "urn:ietf:params:acme:error:unauthorized", "bad sig"); return;
385
- }
386
- const certId = url.replace(/^\/cert\//, "");
387
- const pem = this.certs.get(certId);
388
- if (!pem) { this.sendProblem(res, 404, "urn:ietf:params:acme:error:malformed", "no cert"); return; }
389
-
390
- res.statusCode = 200;
391
- res.setHeader("Content-Type", wire.CONTENT_TYPE_PEM_CERT);
392
- res.setHeader("Replay-Nonce", this.mintNonce());
393
- res.end(pem);
394
- }
395
-
396
- private async handleOrder(req: IncomingMessage, res: ServerResponse, url: string): Promise<void> {
397
- const env = await this.readEnvelope(req, res); if (!env) return;
398
- const header = this.parseHeader(env, res); if (!header) return;
399
- if (!this.consumeNonce(header.nonce)) {
400
- this.sendProblem(res, 400, "urn:ietf:params:acme:error:badNonce", "invalid nonce"); return;
401
- }
402
- if (!this.verifyAccount(env, header)) {
403
- this.sendProblem(res, 401, "urn:ietf:params:acme:error:unauthorized", "bad sig"); return;
404
- }
405
- const orderId = url.replace(/^\/order\//, "");
406
- const os = this.orders.get(orderId);
407
- if (!os) { this.sendProblem(res, 404, "urn:ietf:params:acme:error:malformed", "no order"); return; }
408
- const authzUrl = `${this.baseUrl}/authz/${os.authzId}`;
409
- res.setHeader("Replay-Nonce", this.mintNonce());
410
- this.sendJson(res, 200, {
411
- status: os.status, identifiers: [os.identifier],
412
- authorizations: [authzUrl], finalize: os.finalizeUrl,
413
- certificate: os.certificateUrl,
414
- } as Order);
415
- }
416
-
417
- // ── helpers ──────────────────────────────────────────────────────────────
418
-
419
- private mintNonce(): string {
420
- const n = Jws.b64uEncode(new Uint8Array(randomBytes(16)));
421
- this.nonces.add(n);
422
- return n;
423
- }
424
-
425
- private consumeNonce(nonce: string): boolean {
426
- return this.nonces.delete(nonce);
427
- }
428
-
429
- private verifyAccount(env: Jws.Envelope, header: Jws.ProtectedHeader): boolean {
430
- if (!header.kid) return false;
431
- const jwk = this.accountJwks.get(header.kid);
432
- if (!jwk) return false;
433
- return Jws.verify(env, Jws.publicKeyFromJwk(jwk)) !== null;
434
- }
435
-
436
- private async readEnvelope(req: IncomingMessage, res: ServerResponse): Promise<Jws.Envelope | null> {
437
- try {
438
- const chunks: Buffer[] = [];
439
- for await (const chunk of req) {
440
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
441
- }
442
- const body = Buffer.concat(chunks).toString("utf8");
443
- return JSON.parse(body) as Jws.Envelope;
444
- } catch (e) {
445
- this.sendProblem(res, 400, "urn:ietf:params:acme:error:malformed",
446
- `body read/parse failed: ${(e as Error).message}`);
447
- return null;
448
- }
449
- }
450
-
451
- private parseHeader(env: Jws.Envelope, res: ServerResponse): Jws.ProtectedHeader | null {
452
- try {
453
- return JSON.parse(new TextDecoder().decode(Jws.b64uDecode(env.protected))) as Jws.ProtectedHeader;
454
- } catch (e) {
455
- this.sendProblem(res, 400, "urn:ietf:params:acme:error:malformed",
456
- `malformed protected header: ${(e as Error).message}`);
457
- return null;
458
- }
459
- }
460
-
461
- private sendJson(res: ServerResponse, status: number, body: unknown): void {
462
- res.statusCode = status;
463
- res.setHeader("Content-Type", "application/json");
464
- res.end(JSON.stringify(body));
465
- }
466
-
467
- private sendProblem(res: ServerResponse, status: number, type: string, detail: string): void {
468
- res.statusCode = status;
469
- res.setHeader("Content-Type", wire.CONTENT_TYPE_PROBLEM);
470
- res.end(JSON.stringify({ type, detail, status } as ProblemDetail));
471
- }
472
- }
473
-
474
- function shortId(): string {
475
- return Buffer.from(randomBytes(8)).toString("hex");
476
- }
477
-
478
- function randomHexSerial(): string {
479
- return Buffer.from(randomBytes(20)).toString("hex");
480
- }
@@ -1,24 +0,0 @@
1
- // Copyright 2026 INNO LOTUS PTY LTD
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
- /** ACME wire constants (RFC 8555 + NPS-RFC-0002 §4.4). */
5
-
6
- export const CONTENT_TYPE_JOSE_JSON = "application/jose+json";
7
- export const CONTENT_TYPE_PROBLEM = "application/problem+json";
8
- export const CONTENT_TYPE_PEM_CERT = "application/pem-certificate-chain";
9
-
10
- export const CHALLENGE_AGENT_01 = "agent-01";
11
- export const IDENTIFIER_TYPE_NID = "nid";
12
-
13
- /** ACME status enumeration values (RFC 8555 §7.1.6). */
14
- export const Status = {
15
- PENDING: "pending",
16
- READY: "ready",
17
- PROCESSING: "processing",
18
- VALID: "valid",
19
- INVALID: "invalid",
20
- EXPIRED: "expired",
21
- DEACTIVATED: "deactivated",
22
- REVOKED: "revoked",
23
- SUBMITTED: "submitted",
24
- } as const;
@@ -1,40 +0,0 @@
1
- // Copyright 2026 INNO LOTUS PTY LTD
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
- /** Agent identity assurance level per NPS-RFC-0003 §5.1.1. */
5
- export type AssuranceLevelWire = "anonymous" | "attested" | "verified";
6
-
7
- export class AssuranceLevel {
8
- static readonly ANONYMOUS = new AssuranceLevel("anonymous", 0);
9
- static readonly ATTESTED = new AssuranceLevel("attested", 1);
10
- static readonly VERIFIED = new AssuranceLevel("verified", 2);
11
-
12
- private constructor(
13
- public readonly wire: AssuranceLevelWire,
14
- public readonly rank: number,
15
- ) {}
16
-
17
- meetsOrExceeds(required: AssuranceLevel): boolean {
18
- return this.rank >= required.rank;
19
- }
20
-
21
- /**
22
- * Parse a wire string. `null`, `undefined`, or `""` → `ANONYMOUS`
23
- * (backward compat per NPS-RFC-0003 §5.1.1). Any other unrecognised
24
- * non-empty value throws — callers MUST surface it as `NIP-ASSURANCE-UNKNOWN`.
25
- */
26
- static fromWire(wire: string | null | undefined): AssuranceLevel {
27
- if (!wire) return AssuranceLevel.ANONYMOUS; // null, undefined, or ""
28
- for (const level of [AssuranceLevel.ANONYMOUS, AssuranceLevel.ATTESTED, AssuranceLevel.VERIFIED]) {
29
- if (level.wire === wire) return level;
30
- }
31
- throw new Error(`Unknown assurance_level: ${JSON.stringify(wire)}`);
32
- }
33
-
34
- static fromRank(rank: number): AssuranceLevel {
35
- for (const level of [AssuranceLevel.ANONYMOUS, AssuranceLevel.ATTESTED, AssuranceLevel.VERIFIED]) {
36
- if (level.rank === rank) return level;
37
- }
38
- throw new Error(`Unknown assurance_level rank: ${rank}`);
39
- }
40
- }
@@ -1,9 +0,0 @@
1
- // Copyright 2026 INNO LOTUS PTY LTD
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
- /** Wire-form constants for `IdentFrame.cert_format` (NPS-RFC-0002 §4.5). */
5
-
6
- export const V1_PROPRIETARY = "v1-proprietary" as const;
7
- export const V2_X509 = "v2-x509" as const;
8
-
9
- export type CertFormat = typeof V1_PROPRIETARY | typeof V2_X509;
@@ -1,38 +0,0 @@
1
- // Copyright 2026 INNO LOTUS PTY LTD
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
- /** NIP error code wire constants — mirror of `spec/error-codes.md` NIP section. */
5
-
6
- // ── Cert verification (v1 + v2) ──────────────────────────────────────────────
7
- export const CERT_EXPIRED = "NIP-CERT-EXPIRED";
8
- export const CERT_REVOKED = "NIP-CERT-REVOKED";
9
- export const CERT_SIGNATURE_INVALID = "NIP-CERT-SIGNATURE-INVALID";
10
- export const CERT_UNTRUSTED_ISSUER = "NIP-CERT-UNTRUSTED-ISSUER";
11
- export const CERT_CAPABILITY_MISSING = "NIP-CERT-CAPABILITY-MISSING";
12
- export const CERT_SCOPE_VIOLATION = "NIP-CERT-SCOPE-VIOLATION";
13
-
14
- // ── CA service ───────────────────────────────────────────────────────────────
15
- export const CA_NID_NOT_FOUND = "NIP-CA-NID-NOT-FOUND";
16
- export const CA_NID_ALREADY_EXISTS = "NIP-CA-NID-ALREADY-EXISTS";
17
- export const CA_SERIAL_DUPLICATE = "NIP-CA-SERIAL-DUPLICATE";
18
- export const CA_RENEWAL_TOO_EARLY = "NIP-CA-RENEWAL-TOO-EARLY";
19
- export const CA_SCOPE_EXPANSION_DENIED = "NIP-CA-SCOPE-EXPANSION-DENIED";
20
-
21
- export const OCSP_UNAVAILABLE = "NIP-OCSP-UNAVAILABLE";
22
- export const TRUST_FRAME_INVALID = "NIP-TRUST-FRAME-INVALID";
23
-
24
- // ── RFC-0003 (assurance level) ───────────────────────────────────────────────
25
- export const ASSURANCE_MISMATCH = "NIP-ASSURANCE-MISMATCH";
26
- export const ASSURANCE_UNKNOWN = "NIP-ASSURANCE-UNKNOWN";
27
-
28
- // ── RFC-0004 (reputation log) ────────────────────────────────────────────────
29
- export const REPUTATION_ENTRY_INVALID = "NIP-REPUTATION-ENTRY-INVALID";
30
- export const REPUTATION_LOG_UNREACHABLE = "NIP-REPUTATION-LOG-UNREACHABLE";
31
- export const REPUTATION_GOSSIP_FORK = "NIP-REPUTATION-GOSSIP-FORK";
32
- export const REPUTATION_GOSSIP_SIG_INVALID = "NIP-REPUTATION-GOSSIP-SIG-INVALID";
33
-
34
- // ── RFC-0002 (X.509 + ACME) ──────────────────────────────────────────────────
35
- export const CERT_FORMAT_INVALID = "NIP-CERT-FORMAT-INVALID";
36
- export const CERT_EKU_MISSING = "NIP-CERT-EKU-MISSING";
37
- export const CERT_SUBJECT_NID_MISMATCH = "NIP-CERT-SUBJECT-NID-MISMATCH";
38
- export const ACME_CHALLENGE_FAILED = "NIP-ACME-CHALLENGE-FAILED";