@labacacia/nps-sdk 1.0.0-alpha.2 → 1.0.0-alpha.4

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 (315) hide show
  1. package/CHANGELOG.cn.md +73 -2
  2. package/CHANGELOG.md +82 -2
  3. package/README.cn.md +8 -2
  4. package/README.md +8 -2
  5. package/dist/core/anchor-cache.d.ts +0 -0
  6. package/dist/core/anchor-cache.d.ts.map +0 -0
  7. package/dist/core/anchor-cache.js +0 -0
  8. package/dist/core/anchor-cache.js.map +0 -0
  9. package/dist/core/cache.d.ts +0 -0
  10. package/dist/core/cache.d.ts.map +0 -0
  11. package/dist/core/cache.js +0 -0
  12. package/dist/core/cache.js.map +0 -0
  13. package/dist/core/canonical-json.d.ts +0 -0
  14. package/dist/core/canonical-json.d.ts.map +0 -0
  15. package/dist/core/canonical-json.js +0 -0
  16. package/dist/core/canonical-json.js.map +0 -0
  17. package/dist/core/codec.d.ts +0 -0
  18. package/dist/core/codec.d.ts.map +0 -0
  19. package/dist/core/codec.js +0 -0
  20. package/dist/core/codec.js.map +0 -0
  21. package/dist/core/codecs/index.d.ts +0 -0
  22. package/dist/core/codecs/index.d.ts.map +0 -0
  23. package/dist/core/codecs/index.js +0 -0
  24. package/dist/core/codecs/index.js.map +0 -0
  25. package/dist/core/codecs/ncp-codec.d.ts +0 -0
  26. package/dist/core/codecs/ncp-codec.d.ts.map +0 -0
  27. package/dist/core/codecs/ncp-codec.js +0 -0
  28. package/dist/core/codecs/ncp-codec.js.map +0 -0
  29. package/dist/core/codecs/tier1-json-codec.d.ts +0 -0
  30. package/dist/core/codecs/tier1-json-codec.d.ts.map +0 -0
  31. package/dist/core/codecs/tier1-json-codec.js +0 -0
  32. package/dist/core/codecs/tier1-json-codec.js.map +0 -0
  33. package/dist/core/codecs/tier2-msgpack-codec.d.ts +0 -0
  34. package/dist/core/codecs/tier2-msgpack-codec.d.ts.map +0 -0
  35. package/dist/core/codecs/tier2-msgpack-codec.js +0 -0
  36. package/dist/core/codecs/tier2-msgpack-codec.js.map +0 -0
  37. package/dist/core/crypto-provider.d.ts +0 -0
  38. package/dist/core/crypto-provider.d.ts.map +0 -0
  39. package/dist/core/crypto-provider.js +0 -0
  40. package/dist/core/crypto-provider.js.map +0 -0
  41. package/dist/core/exceptions.d.ts +0 -0
  42. package/dist/core/exceptions.d.ts.map +0 -0
  43. package/dist/core/exceptions.js +0 -0
  44. package/dist/core/exceptions.js.map +0 -0
  45. package/dist/core/frame-header.d.ts +0 -0
  46. package/dist/core/frame-header.d.ts.map +0 -0
  47. package/dist/core/frame-header.js +0 -0
  48. package/dist/core/frame-header.js.map +0 -0
  49. package/dist/core/frame-registry.d.ts +0 -0
  50. package/dist/core/frame-registry.d.ts.map +0 -0
  51. package/dist/core/frame-registry.js +0 -0
  52. package/dist/core/frame-registry.js.map +0 -0
  53. package/dist/core/frames.d.ts +0 -0
  54. package/dist/core/frames.d.ts.map +0 -0
  55. package/dist/core/frames.js +0 -0
  56. package/dist/core/frames.js.map +0 -0
  57. package/dist/core/index.d.ts +0 -0
  58. package/dist/core/index.d.ts.map +0 -0
  59. package/dist/core/index.js +0 -0
  60. package/dist/core/index.js.map +0 -0
  61. package/dist/core/registry.d.ts +0 -0
  62. package/dist/core/registry.d.ts.map +0 -0
  63. package/dist/core/registry.js +0 -0
  64. package/dist/core/registry.js.map +0 -0
  65. package/dist/core/status-codes.d.ts +0 -0
  66. package/dist/core/status-codes.d.ts.map +0 -0
  67. package/dist/core/status-codes.js +0 -0
  68. package/dist/core/status-codes.js.map +0 -0
  69. package/dist/index.d.ts +0 -0
  70. package/dist/index.d.ts.map +0 -0
  71. package/dist/index.js +0 -0
  72. package/dist/index.js.map +0 -0
  73. package/dist/ncp/frames/anchor-frame.d.ts +0 -0
  74. package/dist/ncp/frames/anchor-frame.d.ts.map +0 -0
  75. package/dist/ncp/frames/anchor-frame.js +0 -0
  76. package/dist/ncp/frames/anchor-frame.js.map +0 -0
  77. package/dist/ncp/frames/caps-frame.d.ts +0 -0
  78. package/dist/ncp/frames/caps-frame.d.ts.map +0 -0
  79. package/dist/ncp/frames/caps-frame.js +0 -0
  80. package/dist/ncp/frames/caps-frame.js.map +0 -0
  81. package/dist/ncp/frames/diff-frame.d.ts +0 -0
  82. package/dist/ncp/frames/diff-frame.d.ts.map +0 -0
  83. package/dist/ncp/frames/diff-frame.js +0 -0
  84. package/dist/ncp/frames/diff-frame.js.map +0 -0
  85. package/dist/ncp/frames/error-frame.d.ts +0 -0
  86. package/dist/ncp/frames/error-frame.d.ts.map +0 -0
  87. package/dist/ncp/frames/error-frame.js +0 -0
  88. package/dist/ncp/frames/error-frame.js.map +0 -0
  89. package/dist/ncp/frames/hello-frame.d.ts +0 -0
  90. package/dist/ncp/frames/hello-frame.d.ts.map +0 -0
  91. package/dist/ncp/frames/hello-frame.js +0 -0
  92. package/dist/ncp/frames/hello-frame.js.map +0 -0
  93. package/dist/ncp/frames/stream-frame.d.ts +0 -0
  94. package/dist/ncp/frames/stream-frame.d.ts.map +0 -0
  95. package/dist/ncp/frames/stream-frame.js +0 -0
  96. package/dist/ncp/frames/stream-frame.js.map +0 -0
  97. package/dist/ncp/frames.d.ts +0 -0
  98. package/dist/ncp/frames.d.ts.map +0 -0
  99. package/dist/ncp/frames.js +0 -0
  100. package/dist/ncp/frames.js.map +0 -0
  101. package/dist/ncp/handshake.d.ts +0 -0
  102. package/dist/ncp/handshake.d.ts.map +0 -0
  103. package/dist/ncp/handshake.js +0 -0
  104. package/dist/ncp/handshake.js.map +0 -0
  105. package/dist/ncp/index.d.ts +1 -0
  106. package/dist/ncp/index.d.ts.map +1 -1
  107. package/dist/ncp/index.js +1 -0
  108. package/dist/ncp/index.js.map +1 -1
  109. package/dist/ncp/ncp-error-codes.d.ts +1 -0
  110. package/dist/ncp/ncp-error-codes.d.ts.map +1 -1
  111. package/dist/ncp/ncp-error-codes.js +2 -0
  112. package/dist/ncp/ncp-error-codes.js.map +1 -1
  113. package/dist/ncp/ncp-patch-format.d.ts +0 -0
  114. package/dist/ncp/ncp-patch-format.d.ts.map +0 -0
  115. package/dist/ncp/ncp-patch-format.js +0 -0
  116. package/dist/ncp/ncp-patch-format.js.map +0 -0
  117. package/dist/ncp/preamble.d.ts +47 -0
  118. package/dist/ncp/preamble.d.ts.map +1 -0
  119. package/dist/ncp/preamble.js +74 -0
  120. package/dist/ncp/preamble.js.map +1 -0
  121. package/dist/ncp/registry.d.ts +0 -0
  122. package/dist/ncp/registry.d.ts.map +0 -0
  123. package/dist/ncp/registry.js +0 -0
  124. package/dist/ncp/registry.js.map +0 -0
  125. package/dist/ncp/stream-manager.d.ts +0 -0
  126. package/dist/ncp/stream-manager.d.ts.map +0 -0
  127. package/dist/ncp/stream-manager.js +0 -0
  128. package/dist/ncp/stream-manager.js.map +0 -0
  129. package/dist/ndp/frames.d.ts +0 -0
  130. package/dist/ndp/frames.d.ts.map +0 -0
  131. package/dist/ndp/frames.js +0 -0
  132. package/dist/ndp/frames.js.map +0 -0
  133. package/dist/ndp/index.d.ts +0 -0
  134. package/dist/ndp/index.d.ts.map +0 -0
  135. package/dist/ndp/index.js +0 -0
  136. package/dist/ndp/index.js.map +0 -0
  137. package/dist/ndp/ndp-registry.d.ts +0 -0
  138. package/dist/ndp/ndp-registry.d.ts.map +0 -0
  139. package/dist/ndp/ndp-registry.js +0 -0
  140. package/dist/ndp/ndp-registry.js.map +0 -0
  141. package/dist/ndp/registry.d.ts +0 -0
  142. package/dist/ndp/registry.d.ts.map +0 -0
  143. package/dist/ndp/registry.js +0 -0
  144. package/dist/ndp/registry.js.map +0 -0
  145. package/dist/ndp/validator.d.ts +0 -0
  146. package/dist/ndp/validator.d.ts.map +0 -0
  147. package/dist/ndp/validator.js +0 -0
  148. package/dist/ndp/validator.js.map +0 -0
  149. package/dist/nip/acme/client.d.ts +31 -0
  150. package/dist/nip/acme/client.d.ts.map +1 -0
  151. package/dist/nip/acme/client.js +136 -0
  152. package/dist/nip/acme/client.js.map +1 -0
  153. package/dist/nip/acme/index.d.ts +6 -0
  154. package/dist/nip/acme/index.d.ts.map +1 -0
  155. package/dist/nip/acme/index.js +8 -0
  156. package/dist/nip/acme/index.js.map +1 -0
  157. package/dist/nip/acme/jws.d.ts +31 -0
  158. package/dist/nip/acme/jws.d.ts.map +1 -0
  159. package/dist/nip/acme/jws.js +76 -0
  160. package/dist/nip/acme/jws.js.map +1 -0
  161. package/dist/nip/acme/messages.d.ts +71 -0
  162. package/dist/nip/acme/messages.d.ts.map +1 -0
  163. package/dist/nip/acme/messages.js +4 -0
  164. package/dist/nip/acme/messages.js.map +1 -0
  165. package/dist/nip/acme/server.d.ts +41 -0
  166. package/dist/nip/acme/server.d.ts.map +1 -0
  167. package/dist/nip/acme/server.js +458 -0
  168. package/dist/nip/acme/server.js.map +1 -0
  169. package/dist/nip/acme/wire.d.ts +19 -0
  170. package/dist/nip/acme/wire.d.ts.map +1 -0
  171. package/dist/nip/acme/wire.js +21 -0
  172. package/dist/nip/acme/wire.js.map +1 -0
  173. package/dist/nip/assurance-level.d.ts +14 -0
  174. package/dist/nip/assurance-level.d.ts.map +1 -0
  175. package/dist/nip/assurance-level.js +33 -0
  176. package/dist/nip/assurance-level.js.map +1 -0
  177. package/dist/nip/cert-format.d.ts +5 -0
  178. package/dist/nip/cert-format.d.ts.map +1 -0
  179. package/dist/nip/cert-format.js +6 -0
  180. package/dist/nip/cert-format.js.map +1 -0
  181. package/dist/nip/error-codes.d.ts +23 -0
  182. package/dist/nip/error-codes.d.ts.map +1 -0
  183. package/dist/nip/error-codes.js +30 -0
  184. package/dist/nip/error-codes.js.map +1 -0
  185. package/dist/nip/frames.d.ts +10 -1
  186. package/dist/nip/frames.d.ts.map +1 -1
  187. package/dist/nip/frames.js +29 -4
  188. package/dist/nip/frames.js.map +1 -1
  189. package/dist/nip/identity.d.ts +0 -0
  190. package/dist/nip/identity.d.ts.map +0 -0
  191. package/dist/nip/identity.js +0 -0
  192. package/dist/nip/identity.js.map +0 -0
  193. package/dist/nip/index.d.ts +6 -0
  194. package/dist/nip/index.d.ts.map +1 -1
  195. package/dist/nip/index.js +7 -0
  196. package/dist/nip/index.js.map +1 -1
  197. package/dist/nip/registry.d.ts +0 -0
  198. package/dist/nip/registry.d.ts.map +0 -0
  199. package/dist/nip/registry.js +0 -0
  200. package/dist/nip/registry.js.map +0 -0
  201. package/dist/nip/verifier.d.ts +23 -0
  202. package/dist/nip/verifier.d.ts.map +1 -0
  203. package/dist/nip/verifier.js +90 -0
  204. package/dist/nip/verifier.js.map +1 -0
  205. package/dist/nip/x509/builder.d.ts +35 -0
  206. package/dist/nip/x509/builder.d.ts.map +1 -0
  207. package/dist/nip/x509/builder.js +59 -0
  208. package/dist/nip/x509/builder.js.map +1 -0
  209. package/dist/nip/x509/index.d.ts +4 -0
  210. package/dist/nip/x509/index.d.ts.map +1 -0
  211. package/dist/nip/x509/index.js +6 -0
  212. package/dist/nip/x509/index.js.map +1 -0
  213. package/dist/nip/x509/oids.d.ts +17 -0
  214. package/dist/nip/x509/oids.d.ts.map +1 -0
  215. package/dist/nip/x509/oids.js +23 -0
  216. package/dist/nip/x509/oids.js.map +1 -0
  217. package/dist/nip/x509/verifier.d.ts +26 -0
  218. package/dist/nip/x509/verifier.d.ts.map +1 -0
  219. package/dist/nip/x509/verifier.js +171 -0
  220. package/dist/nip/x509/verifier.js.map +1 -0
  221. package/dist/nop/client.d.ts +0 -0
  222. package/dist/nop/client.d.ts.map +0 -0
  223. package/dist/nop/client.js +0 -0
  224. package/dist/nop/client.js.map +0 -0
  225. package/dist/nop/frames.d.ts +0 -0
  226. package/dist/nop/frames.d.ts.map +0 -0
  227. package/dist/nop/frames.js +0 -0
  228. package/dist/nop/frames.js.map +0 -0
  229. package/dist/nop/index.d.ts +0 -0
  230. package/dist/nop/index.d.ts.map +0 -0
  231. package/dist/nop/index.js +0 -0
  232. package/dist/nop/index.js.map +0 -0
  233. package/dist/nop/models.d.ts +0 -0
  234. package/dist/nop/models.d.ts.map +0 -0
  235. package/dist/nop/models.js +0 -0
  236. package/dist/nop/models.js.map +0 -0
  237. package/dist/nop/nop-types.d.ts +0 -0
  238. package/dist/nop/nop-types.d.ts.map +0 -0
  239. package/dist/nop/nop-types.js +0 -0
  240. package/dist/nop/nop-types.js.map +0 -0
  241. package/dist/nop/registry.d.ts +0 -0
  242. package/dist/nop/registry.d.ts.map +0 -0
  243. package/dist/nop/registry.js +0 -0
  244. package/dist/nop/registry.js.map +0 -0
  245. package/dist/nwp/client.d.ts +0 -0
  246. package/dist/nwp/client.d.ts.map +0 -0
  247. package/dist/nwp/client.js +0 -0
  248. package/dist/nwp/client.js.map +0 -0
  249. package/dist/nwp/frames.d.ts +0 -0
  250. package/dist/nwp/frames.d.ts.map +0 -0
  251. package/dist/nwp/frames.js +0 -0
  252. package/dist/nwp/frames.js.map +0 -0
  253. package/dist/nwp/index.d.ts +0 -0
  254. package/dist/nwp/index.d.ts.map +0 -0
  255. package/dist/nwp/index.js +0 -0
  256. package/dist/nwp/index.js.map +0 -0
  257. package/dist/nwp/registry.d.ts +0 -0
  258. package/dist/nwp/registry.d.ts.map +0 -0
  259. package/dist/nwp/registry.js +0 -0
  260. package/dist/nwp/registry.js.map +0 -0
  261. package/dist/setup.d.ts +0 -0
  262. package/dist/setup.d.ts.map +0 -0
  263. package/dist/setup.js +0 -0
  264. package/dist/setup.js.map +0 -0
  265. package/package.json +2 -1
  266. package/src/index.ts +0 -0
  267. package/src/ncp/index.ts +1 -0
  268. package/src/ncp/ncp-error-codes.ts +2 -0
  269. package/src/ncp/preamble.ts +79 -0
  270. package/src/nip/acme/client.ts +185 -0
  271. package/src/nip/acme/index.ts +8 -0
  272. package/src/nip/acme/jws.ts +109 -0
  273. package/src/nip/acme/messages.ts +85 -0
  274. package/src/nip/acme/server.ts +480 -0
  275. package/src/nip/acme/wire.ts +24 -0
  276. package/src/nip/assurance-level.ts +35 -0
  277. package/src/nip/cert-format.ts +9 -0
  278. package/src/nip/error-codes.ts +36 -0
  279. package/src/nip/frames.ts +35 -3
  280. package/src/nip/index.ts +8 -0
  281. package/src/nip/verifier.ts +122 -0
  282. package/src/nip/x509/builder.ts +91 -0
  283. package/src/nip/x509/index.ts +6 -0
  284. package/src/nip/x509/oids.ts +28 -0
  285. package/src/nip/x509/verifier.ts +214 -0
  286. package/tests/_rfc0002-keys.ts +57 -0
  287. package/tests/ncp/preamble.test.ts +93 -0
  288. package/tests/nip-acme-agent01.test.ts +192 -0
  289. package/tests/nip-x509.test.ts +280 -0
  290. package/.npmrc.publish +0 -1
  291. package/dist/codec-CmHeovTV.d.cts +0 -120
  292. package/dist/codec-CmHeovTV.d.ts +0 -120
  293. package/dist/core/index.cjs +0 -371
  294. package/dist/core/index.cjs.map +0 -1
  295. package/dist/core/index.d.cts +0 -41
  296. package/dist/frames-B3qLdl_g.d.cts +0 -77
  297. package/dist/frames-Ff7-ZPUl.d.ts +0 -77
  298. package/dist/index.cjs +0 -1556
  299. package/dist/index.cjs.map +0 -1
  300. package/dist/index.d.cts +0 -21
  301. package/dist/ncp/index.cjs +0 -188
  302. package/dist/ncp/index.cjs.map +0 -1
  303. package/dist/ncp/index.d.cts +0 -6
  304. package/dist/ndp/index.cjs +0 -252
  305. package/dist/ndp/index.cjs.map +0 -1
  306. package/dist/ndp/index.d.cts +0 -86
  307. package/dist/nip/index.cjs +0 -214
  308. package/dist/nip/index.cjs.map +0 -1
  309. package/dist/nip/index.d.cts +0 -65
  310. package/dist/nop/index.cjs +0 -762
  311. package/dist/nop/index.cjs.map +0 -1
  312. package/dist/nop/index.d.cts +0 -155
  313. package/dist/nwp/index.cjs +0 -658
  314. package/dist/nwp/index.cjs.map +0 -1
  315. package/dist/nwp/index.d.cts +0 -65
@@ -0,0 +1,171 @@
1
+ // Copyright 2026 INNO LOTUS PTY LTD
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Verifies NPS X.509 NID certificate chains per NPS-RFC-0002 §4.
5
+ *
6
+ * Stages (RFC §4.6):
7
+ * 1. Decode chain (base64url DER → @peculiar/x509 X509Certificate).
8
+ * 2. Leaf EKU check — critical, contains agent-identity OR node-identity OID.
9
+ * 3. Subject CN / SAN URI match against asserted NID.
10
+ * 4. Assurance-level extension match against asserted level (if both present).
11
+ * 5. Chain signature verification — leaf → intermediates → trusted root.
12
+ */
13
+ import * as x509 from "@peculiar/x509";
14
+ import { AssuranceLevel } from "../assurance-level.js";
15
+ import * as ec from "../error-codes.js";
16
+ import { EKU_AGENT_IDENTITY, EKU_NODE_IDENTITY, NID_ASSURANCE_LEVEL, OID_EXTENDED_KEY_USAGE, } from "./oids.js";
17
+ x509.cryptoProvider.set(globalThis.crypto);
18
+ function ok(leaf) {
19
+ return { valid: true, leaf };
20
+ }
21
+ function fail(errorCode, message) {
22
+ return { valid: false, errorCode, message };
23
+ }
24
+ export async function verify(opts) {
25
+ // Stage 1: decode chain ─────────────────────────────────────────────────
26
+ if (!opts.certChainBase64UrlDer.length) {
27
+ return fail(ec.CERT_FORMAT_INVALID, "cert_chain is empty");
28
+ }
29
+ let chain;
30
+ try {
31
+ chain = opts.certChainBase64UrlDer.map((s) => new x509.X509Certificate(b64uDecode(s).buffer));
32
+ }
33
+ catch (e) {
34
+ return fail(ec.CERT_FORMAT_INVALID, `DER decode failed: ${e.message}`);
35
+ }
36
+ const leaf = chain[0];
37
+ // Stage 2: EKU check ────────────────────────────────────────────────────
38
+ const ekuResult = checkLeafEku(leaf);
39
+ if (!ekuResult.valid)
40
+ return ekuResult;
41
+ // Stage 3: subject CN / SAN URI match ──────────────────────────────────
42
+ const subjectResult = checkSubjectNid(leaf, opts.assertedNid);
43
+ if (!subjectResult.valid)
44
+ return subjectResult;
45
+ // Stage 4: assurance-level extension ───────────────────────────────────
46
+ const assuranceResult = checkAssuranceLevel(leaf, opts.assertedAssuranceLevel);
47
+ if (!assuranceResult.valid)
48
+ return assuranceResult;
49
+ // Stage 5: chain signature verification ────────────────────────────────
50
+ const chainResult = await checkChainSignature(chain, opts.trustedRootCerts);
51
+ if (!chainResult.valid)
52
+ return chainResult;
53
+ return ok(leaf);
54
+ }
55
+ // ── Stage helpers ──────────────────────────────────────────────────────────
56
+ function checkLeafEku(leaf) {
57
+ const ekuExt = leaf.extensions.find((e) => e.type === OID_EXTENDED_KEY_USAGE);
58
+ if (!ekuExt) {
59
+ return fail(ec.CERT_EKU_MISSING, "leaf has no ExtendedKeyUsage extension");
60
+ }
61
+ if (!ekuExt.critical) {
62
+ return fail(ec.CERT_EKU_MISSING, "ExtendedKeyUsage extension is not marked critical");
63
+ }
64
+ const usages = ekuExt.usages;
65
+ if (!usages.includes(EKU_AGENT_IDENTITY) && !usages.includes(EKU_NODE_IDENTITY)) {
66
+ return fail(ec.CERT_EKU_MISSING, "ExtendedKeyUsage does not contain agent-identity or node-identity OID");
67
+ }
68
+ return ok(leaf);
69
+ }
70
+ function checkSubjectNid(leaf, assertedNid) {
71
+ // @peculiar/x509 parses the subject DN; iterate to find CN.
72
+ const cn = extractCn(leaf.subject);
73
+ if (cn !== assertedNid) {
74
+ return fail(ec.CERT_SUBJECT_NID_MISMATCH, `leaf subject CN (${cn ?? "<missing>"}) does not match asserted NID (${assertedNid})`);
75
+ }
76
+ const sanExt = leaf.getExtension(x509.SubjectAlternativeNameExtension);
77
+ if (!sanExt) {
78
+ return fail(ec.CERT_SUBJECT_NID_MISMATCH, "leaf has no Subject Alternative Name extension");
79
+ }
80
+ // SubjectAlternativeNameExtension exposes general-name objects with `type: "url"` for URIs.
81
+ const uris = sanExt.names
82
+ .toJSON()
83
+ .filter((n) => n.type === "url")
84
+ .map((n) => n.value);
85
+ if (!uris.includes(assertedNid)) {
86
+ return fail(ec.CERT_SUBJECT_NID_MISMATCH, "no SAN URI matches asserted NID");
87
+ }
88
+ return ok(leaf);
89
+ }
90
+ function checkAssuranceLevel(leaf, asserted) {
91
+ if (asserted === null)
92
+ return ok(leaf);
93
+ const ext = leaf.extensions.find((e) => e.type === NID_ASSURANCE_LEVEL);
94
+ if (!ext) {
95
+ // Optional in v0.1 — pass silently.
96
+ return ok(leaf);
97
+ }
98
+ const der = new Uint8Array(ext.value);
99
+ // Decode ASN.1 ENUMERATED: tag=0x0A, len=0x01, content=<rank>.
100
+ if (der.length !== 3 || der[0] !== 0x0A || der[1] !== 0x01) {
101
+ return fail(ec.CERT_FORMAT_INVALID, `malformed assurance-level extension: ${Buffer.from(der).toString("hex")}`);
102
+ }
103
+ const rank = der[2];
104
+ let certLevel;
105
+ try {
106
+ certLevel = AssuranceLevel.fromRank(rank);
107
+ }
108
+ catch {
109
+ return fail(ec.ASSURANCE_UNKNOWN, `assurance-level extension contains unknown value: ${rank}`);
110
+ }
111
+ if (certLevel !== asserted) {
112
+ return fail(ec.ASSURANCE_MISMATCH, `cert assurance-level (${certLevel.wire}) does not match asserted (${asserted.wire})`);
113
+ }
114
+ return ok(leaf);
115
+ }
116
+ async function checkChainSignature(chain, trustedRoots) {
117
+ if (!trustedRoots.length) {
118
+ return fail(ec.CERT_FORMAT_INVALID, "no trusted X.509 roots configured");
119
+ }
120
+ try {
121
+ // Walk leaf → intermediates: each must be signed by its successor.
122
+ for (let i = 0; i < chain.length - 1; i++) {
123
+ const okStep = await chain[i].verify({ publicKey: await chain[i + 1].publicKey.export(), signatureOnly: true });
124
+ if (!okStep) {
125
+ return fail(ec.CERT_FORMAT_INVALID, `chain link ${i} signature did not verify`);
126
+ }
127
+ }
128
+ // The last cert in the chain MUST chain to a trusted root.
129
+ const last = chain[chain.length - 1];
130
+ for (const root of trustedRoots) {
131
+ if (Buffer.from(last.rawData).equals(Buffer.from(root.rawData))) {
132
+ return ok(chain[0]);
133
+ }
134
+ try {
135
+ const okStep = await last.verify({ publicKey: await root.publicKey.export(), signatureOnly: true });
136
+ if (okStep)
137
+ return ok(chain[0]);
138
+ }
139
+ catch {
140
+ /* try next root */
141
+ }
142
+ }
143
+ return fail(ec.CERT_FORMAT_INVALID, "chain does not anchor to any trusted root");
144
+ }
145
+ catch (e) {
146
+ return fail(ec.CERT_FORMAT_INVALID, `chain signature verification error: ${e.message}`);
147
+ }
148
+ }
149
+ // ── helpers ────────────────────────────────────────────────────────────────
150
+ function extractCn(dn) {
151
+ // @peculiar/x509 returns DN strings in RFC 4514 format ("CN=...,O=...").
152
+ for (const rdn of dn.split(",")) {
153
+ const trimmed = rdn.trim();
154
+ if (trimmed.startsWith("CN=")) {
155
+ let value = trimmed.slice(3);
156
+ // Strip surrounding quotes if any.
157
+ if (value.startsWith("\"") && value.endsWith("\"")) {
158
+ value = value.slice(1, -1);
159
+ }
160
+ // Unescape.
161
+ return value.replace(/\\([",+;<>\\])/g, "$1");
162
+ }
163
+ }
164
+ return null;
165
+ }
166
+ function b64uDecode(s) {
167
+ const padded = s + "=".repeat((4 - (s.length % 4)) % 4);
168
+ const std = padded.replace(/-/g, "+").replace(/_/g, "/");
169
+ return new Uint8Array(Buffer.from(std, "base64"));
170
+ }
171
+ //# sourceMappingURL=verifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verifier.js","sourceRoot":"","sources":["../../../src/nip/x509/verifier.ts"],"names":[],"mappings":"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"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/dist/nop/index.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/dist/nwp/index.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/dist/setup.d.ts CHANGED
File without changes
File without changes
package/dist/setup.js CHANGED
File without changes
package/dist/setup.js.map CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@labacacia/nps-sdk",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.4",
4
4
  "description": "TypeScript SDK for the Neural Protocol Suite (NPS) — NCP, NWP, NIP, NDP, NOP",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -53,6 +53,7 @@
53
53
  "@msgpack/msgpack": "^3.0.0",
54
54
  "@noble/ed25519": "^2.1.0",
55
55
  "@noble/hashes": "^1.4.0",
56
+ "@peculiar/x509": "^1.12.0",
56
57
  "canonicalize": "^2.0.0",
57
58
  "fast-json-patch": "^3.1.1"
58
59
  },
package/src/index.ts CHANGED
File without changes
package/src/ncp/index.ts CHANGED
@@ -10,3 +10,4 @@ export * from "./ncp-error-codes.js";
10
10
  export * from "./ncp-patch-format.js";
11
11
  export * from "./handshake.js";
12
12
  export * from "./stream-manager.js";
13
+ export * from "./preamble.js";
@@ -12,6 +12,8 @@ export const NCP_ERROR_CODES = {
12
12
  // Implementation-only codes (not in spec §6 — see test_results.md spec question 2)
13
13
  NCP_FRAME_PARSE_ERROR: "NCP-FRAME-PARSE-ERROR",
14
14
  NCP_FRAME_INCOMPLETE: "NCP-FRAME-INCOMPLETE",
15
+ // NPS-RFC-0001 — native-mode preamble
16
+ NCP_PREAMBLE_INVALID: "NCP-PREAMBLE-INVALID",
15
17
  // Spec-defined codes
16
18
  NCP_FRAME_UNKNOWN_TYPE: "NCP-FRAME-UNKNOWN-TYPE",
17
19
  NCP_FRAME_PAYLOAD_TOO_LARGE: "NCP-FRAME-PAYLOAD-TOO-LARGE",
@@ -0,0 +1,79 @@
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
+ }
@@ -0,0 +1,185 @@
1
+ // Copyright 2026 INNO LOTUS PTY LTD
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * ACME client implementing the `agent-01` challenge type per NPS-RFC-0002 §4.4.
6
+ *
7
+ * Flow: newNonce → newAccount → newOrder → fetch authz → sign challenge token →
8
+ * finalize with CSR → fetch leaf cert.
9
+ */
10
+
11
+ import * as ed25519 from "@noble/ed25519";
12
+ import { sha512 } from "@noble/hashes/sha512";
13
+ import * as x509 from "@peculiar/x509";
14
+
15
+ import * as Jws from "./jws.js";
16
+ import type {
17
+ Authorization, Challenge, Directory, FinalizePayload,
18
+ Identifier, NewAccountPayload, NewOrderPayload, Order,
19
+ } from "./messages.js";
20
+ import * as wire from "./wire.js";
21
+
22
+ ed25519.etc.sha512Sync = (...m) => sha512(ed25519.etc.concatBytes(...m));
23
+ x509.cryptoProvider.set(globalThis.crypto);
24
+
25
+ export interface AcmeClientOptions {
26
+ /** ACME directory URL. */
27
+ directoryUrl: string;
28
+ /** Account/agent Ed25519 private key (32-byte raw). */
29
+ privateKey: Uint8Array;
30
+ /** Account/agent Ed25519 public key (32-byte raw). */
31
+ publicKey: Uint8Array;
32
+ /** Web Crypto Ed25519 keypair for CSR signing (must match privateKey). */
33
+ webCryptoKeys: CryptoKeyPair;
34
+ }
35
+
36
+ export class AcmeClient {
37
+ private directory: Directory | null = null;
38
+ private accountUrl: string | null = null;
39
+ private lastNonce: string | null = null;
40
+
41
+ constructor(public readonly options: AcmeClientOptions) {}
42
+
43
+ /** Drive the full agent-01 flow for `nid`. Returns issued PEM cert chain. */
44
+ async issueAgentCert(nid: string): Promise<string> {
45
+ await this.ensureDirectory();
46
+ if (this.accountUrl === null) await this.newAccount();
47
+ const order = await this.newOrder(nid);
48
+ const authz = await this.fetchAuthz(order.authorizations[0]);
49
+ await this.respondAgent01(authz);
50
+ const finalized = await this.finalizeOrder(order, nid);
51
+ return this.downloadPem(finalized.certificate!);
52
+ }
53
+
54
+ // ── Stages ───────────────────────────────────────────────────────────────
55
+
56
+ private async ensureDirectory(): Promise<void> {
57
+ if (this.directory !== null) return;
58
+ const resp = await fetch(this.options.directoryUrl);
59
+ ensureSuccess(resp);
60
+ this.directory = await resp.json() as Directory;
61
+ await this.refreshNonce();
62
+ }
63
+
64
+ private async refreshNonce(): Promise<void> {
65
+ const resp = await fetch(this.directory!.newNonce, { method: "HEAD" });
66
+ ensureSuccess(resp);
67
+ this.lastNonce = resp.headers.get("Replay-Nonce");
68
+ if (this.lastNonce === null) {
69
+ throw new Error("server omitted Replay-Nonce");
70
+ }
71
+ }
72
+
73
+ private async newAccount(): Promise<void> {
74
+ const jwk = Jws.jwkFromPublicKey(this.options.publicKey);
75
+ const env = Jws.sign(
76
+ { alg: Jws.ALG_EDDSA, nonce: this.lastNonce!, url: this.directory!.newAccount, jwk },
77
+ { termsOfServiceAgreed: true } as NewAccountPayload,
78
+ this.options.privateKey);
79
+
80
+ const resp = await this.post(this.directory!.newAccount, env);
81
+ ensureSuccess(resp);
82
+ this.accountUrl = resp.headers.get("Location");
83
+ if (this.accountUrl === null) throw new Error("server omitted account Location");
84
+ this.captureNonce(resp);
85
+ }
86
+
87
+ private async newOrder(nid: string): Promise<Order> {
88
+ const env = Jws.sign(
89
+ { alg: Jws.ALG_EDDSA, nonce: this.lastNonce!, url: this.directory!.newOrder, kid: this.accountUrl! },
90
+ {
91
+ identifiers: [{ type: wire.IDENTIFIER_TYPE_NID, value: nid } as Identifier],
92
+ } as NewOrderPayload,
93
+ this.options.privateKey);
94
+
95
+ const resp = await this.post(this.directory!.newOrder, env);
96
+ ensureSuccess(resp);
97
+ this.captureNonce(resp);
98
+ return await resp.json() as Order;
99
+ }
100
+
101
+ private async fetchAuthz(url: string): Promise<Authorization> {
102
+ // POST-as-GET (RFC 8555 §6.3).
103
+ const env = Jws.sign(
104
+ { alg: Jws.ALG_EDDSA, nonce: this.lastNonce!, url, kid: this.accountUrl! },
105
+ null,
106
+ this.options.privateKey);
107
+ const resp = await this.post(url, env);
108
+ ensureSuccess(resp);
109
+ this.captureNonce(resp);
110
+ return await resp.json() as Authorization;
111
+ }
112
+
113
+ private async respondAgent01(authz: Authorization): Promise<void> {
114
+ const challenge = authz.challenges.find((c) => c.type === wire.CHALLENGE_AGENT_01);
115
+ if (!challenge) throw new Error("authz has no agent-01 challenge");
116
+
117
+ // Sign the challenge token with the account/NID private key.
118
+ const tokenBytes = new TextEncoder().encode(challenge.token);
119
+ const sig = ed25519.sign(tokenBytes, this.options.privateKey);
120
+
121
+ const env = Jws.sign(
122
+ { alg: Jws.ALG_EDDSA, nonce: this.lastNonce!, url: challenge.url, kid: this.accountUrl! },
123
+ { agent_signature: Jws.b64uEncode(sig) },
124
+ this.options.privateKey);
125
+ const resp = await this.post(challenge.url, env);
126
+ ensureSuccess(resp);
127
+ this.captureNonce(resp);
128
+ }
129
+
130
+ private async finalizeOrder(order: Order, nid: string): Promise<Order> {
131
+ const csrDer = await this.buildCsr(nid);
132
+ const env = Jws.sign(
133
+ { alg: Jws.ALG_EDDSA, nonce: this.lastNonce!, url: order.finalize, kid: this.accountUrl! },
134
+ { csr: Jws.b64uEncode(csrDer) } as FinalizePayload,
135
+ this.options.privateKey);
136
+ const resp = await this.post(order.finalize, env);
137
+ ensureSuccess(resp);
138
+ this.captureNonce(resp);
139
+ return await resp.json() as Order;
140
+ }
141
+
142
+ private async downloadPem(certUrl: string): Promise<string> {
143
+ const env = Jws.sign(
144
+ { alg: Jws.ALG_EDDSA, nonce: this.lastNonce!, url: certUrl, kid: this.accountUrl! },
145
+ null,
146
+ this.options.privateKey);
147
+ const resp = await this.post(certUrl, env);
148
+ ensureSuccess(resp);
149
+ this.captureNonce(resp);
150
+ return await resp.text();
151
+ }
152
+
153
+ // ── helpers ──────────────────────────────────────────────────────────────
154
+
155
+ private async post(url: string, env: Jws.Envelope): Promise<Response> {
156
+ return await fetch(url, {
157
+ method: "POST",
158
+ headers: { "Content-Type": wire.CONTENT_TYPE_JOSE_JSON },
159
+ body: JSON.stringify(env),
160
+ });
161
+ }
162
+
163
+ private captureNonce(resp: Response): void {
164
+ const nonce = resp.headers.get("Replay-Nonce");
165
+ if (nonce !== null) this.lastNonce = nonce;
166
+ }
167
+
168
+ private async buildCsr(nid: string): Promise<Uint8Array> {
169
+ const csr = await x509.Pkcs10CertificateRequestGenerator.create({
170
+ name: `CN=${nid.replace(/([",+;<>\\])/g, "\\$1")}`,
171
+ keys: this.options.webCryptoKeys,
172
+ signingAlgorithm: { name: "Ed25519" },
173
+ extensions: [
174
+ new x509.SubjectAlternativeNameExtension([{ type: "url", value: nid }], false),
175
+ ],
176
+ });
177
+ return new Uint8Array(csr.rawData);
178
+ }
179
+ }
180
+
181
+ function ensureSuccess(resp: Response): void {
182
+ if (!resp.ok) {
183
+ throw new Error(`ACME ${resp.url} HTTP ${resp.status}`);
184
+ }
185
+ }
@@ -0,0 +1,8 @@
1
+ // Copyright 2026 INNO LOTUS PTY LTD
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ export * from "./client.js";
5
+ export * from "./jws.js";
6
+ export * from "./messages.js";
7
+ export * from "./server.js";
8
+ export * from "./wire.js";