@eternl/ hub- core
Eternl Hub SDKs的共享基础设施. 此套件提供了两者共同使用的基础 @eternl/hub-dapp-sdk 财务报告和审定财务报表 @eternl/hub-wallet-sdk:加密的传输,密码握手,有线协议,以及将它们联系在一起的对称会话层.
这是一个低级的软件包。 大多数应用代码应该使用角色特定的SDK(dApp / 钱包),而不是用手来给这些原始人接线. 原始人在此被记载为SDK作者和高级融合.
安装
npm install @eternl/hub-core
# or
pnpm add @eternl/hub-core
所需资源
- 运行时间与 网络加密 API (单位:千美元)
globalThis.crypto.subtle: 浏览器,或 节点.js 18密码模块调用assertCryptoCapabilities()并抛出环境 没有它. - 无害环境管理或CJS的消费者均得到支持(双管齐下)
import编号 :require以类型构建).
概览
这套材料分为以下几个领域:
- 密码 - ECDH P-256钥匙交换+AES-256-GCM AEAD,带有PIN捆绑的密钥导出和用于MITM保护的6位短认证字符串(SAS6).
- 会议 —— 说
createPairSession,拥有有线信封的对称协议层,加密/解密,心跳,出场乒乓球,并重放验证一对. - 运输 —— 说
NatsTransport(NATS WebSocket) 一个小后面Transport用于依赖性注入的界面。 A 类MockTransport测试双重机可从@eternl/hub-core/testing子路径. - 信件 ——打出的信息定义,类型守护,以及
create…dApp 钱包协议工厂. - 出席情况 ——为JetStream-stream-stream-base 存在协议的主体构建器,头键,和cadence常数.
- 生命周期 —— 说
LifecycleDispatcher,一个单调,它将DOM/应用生命周期事件(tab隐藏/可见,页面隐藏/显示,应用背景/前方)向存在信标进行播放. - 类型 / Utils / 出错 / 常数 共享类型Script类型、编码/事件帮助器、
EternlConnectError类,和协议配置.
进口和子路径出口
生产原始从包根再出口:
import { createSessionEncryption, createPairSession, NatsTransport } from '@eternl/hub-core';
每个地区也可作为进口较窄的次路径出口:
import { createSessionEncryption } from '@eternl/hub-core/crypto';
import { EternlConnectError } from '@eternl/hub-core/errors';
import { createApiCall } from '@eternl/hub-core/messages';
import { createPairSession } from '@eternl/hub-core/session';
import { NatsTransport } from '@eternl/hub-core/transport';
import { LifecycleDispatcher } from '@eternl/hub-core/lifecycle';
import { presencePublishSubject } from '@eternl/hub-core/presence';
import type { AccountInfo } from '@eternl/hub-core/types';
import { generateNonce } from '@eternl/hub-core/utils';
有效的子路径是 ./crypto, (中文(简体) ), ./errors, (中文(简体) ), ./messages, (中文(简体) ), ./session, (中文(简体) ), ./presence, (中文(简体) ), ./lifecycle, (中文(简体) ), ./transport, (中文(简体) ), ./types,以及 ./utils。 。 。 .
测试双等
MockTransport活在奉献下@eternl/hub-core/testing子路径和故意 没有 生产根出口的一部分:import { MockTransport } from '@eternl/hub-core/testing';
密码
密码模块既暴露出低等原始人(generateKeyPair, (中文(简体) ), deriveSharedSecret, (中文(简体) ), deriveSessionKey, (中文(简体) ), encrypt, (中文(简体) ), decrypt, (中文(简体) ), deriveSAS6,.)和通过工厂创建的状态会话包装.
SessionEncryption 实值 没有 一个建筑师。 这是一个小物体暴露 { fromRaw } (别名用于: createSessionEncryptionFromRaw) (中文(简体) ). 握手驱动的实例与 createSessionEncryption(pairingId, pin) 工厂:
import { createSessionEncryption } from '@eternl/hub-core';
import { bytesToHex } from '@eternl/hub-core'; // also on '@eternl/hub-core/utils'
// Each side creates an encryption context bound to the shared pairingId + PIN.
const enc = createSessionEncryption(pairingId, pin);
// 1. Generate this side's ephemeral key pair; returns the raw public key bytes.
const myPublicKey = await enc.initialize();
const myPublicKeyHex = bytesToHex(myPublicKey); // send this to the peer
// (equivalently: enc.getPublicKeyHex())
// 2. After receiving the peer's public key (hex), complete the handshake.
// completeHandshake takes a SINGLE argument: the peer public key as hex.
await enc.completeHandshake(peerPublicKeyHex);
// 3. Both sides display the 6-digit SAS for out-of-band confirmation.
const sas6 = enc.getSAS6(); // throws if called before completeHandshake
// 4. Encrypt. encrypt() returns { data, seq } — `seq` is a per-direction
// monotonic counter that must travel alongside the ciphertext on the wire.
const { data, seq } = await enc.encrypt(new TextEncoder().encode('hello'));
// 5. The receiver decrypts using the same seq it read off the wire.
const plaintext = await enc.decrypt(data, seq);
// When finished, zero out the in-memory key material.
enc.dispose();
关键点(经核实与 src/crypto/index.ts:
createSessionEncryption(pairingId, pin)—— 说pin也许null只有在从持久性状态重建实例时(见下文); 新的握手需要非无核PIN.initialize()返回时Promise<Uint8Array>(原始公用钥匙);getPublicKeyHex()返回与十六进制相同的密钥.completeHandshake(peerPublicKeyHex)只能用一个理由.encrypt(plaintext)决定:{ data: Uint8Array; seq: bigint }财务报告和审定财务报表 需求 握手完成; 称之为更早的掷出.decrypt(data, seq)要求seq是用密码传送的.getSAS6()掷出至completeHandshake已经运行.
从长期状态重建
在不重运行握手( 如重装后) 的情况下恢复加密上下文, 使用 SessionEncryption.fromRaw 编号 : createSessionEncryptionFromRaw数字 :
import { SessionEncryption } from '@eternl/hub-core';
const enc = await SessionEncryption.fromRaw({
pairingId,
sessionKeyRaw, // 32-byte AES key
pinAadBytes, // PIN-hash AAD bytes captured during the original handshake
});
// `enc` can encrypt/decrypt immediately, but cannot run completeHandshake again.
会议
createPairSession 是对称协议层。 它是一个 SessionEncryptionApi 加一个 Transport,拥有有线信封(一个8字节的大-endian) seq aES-GCM blob 之后的前缀,在 hub.pair.v2.<pairId>,运行心跳,自动复制到存在 pings,并验证每个输入框上的回放/protocol-version.
import {
createSessionEncryption,
createPairSession,
NatsTransport,
} from '@eternl/hub-core';
const transport = new NatsTransport();
await transport.connect('wss://ws.hub.de-5.eternl.art'); // or DEFAULT_HUB_URL
const encryption = createSessionEncryption(pairId, pin);
await encryption.initialize();
// ... exchange public keys and call encryption.completeHandshake(peerPublicKeyHex) ...
const session = createPairSession({
pairId,
encryption,
transport,
enableHeartbeat: true, // default
});
// Listen for decrypted, validated inbound messages.
const off = session.on('message', ({ message, type }) => {
// handle role-specific protocol messages (api-call/api-response, etc.)
});
// Send any protocol message object; it is stamped, encoded, encrypted, and published.
await session.send(someProtocolMessage);
// Active presence probe — resolves true if the peer pongs within the timeout.
const online = await session.requestPresenceCheck();
// Cleanup (also available via `using` / Symbol.dispose).
off();
session.dispose();
createPairSession 也发出命名事件,用于可观察性和生命周期反应: presencePing, (中文(简体) ), presencePong, (中文(简体) ), heartbeatReceived, (中文(简体) ), peerPresence, (中文(简体) ), peerDisconnect, (中文(简体) ), peerPairingRemoved,以及 error。角色层( dApp 和钱包 SDKs) 'message' 活动.
运输
import { NatsTransport } from '@eternl/hub-core';
const transport = new NatsTransport();
await transport.connect('wss://ws.hub.de-5.eternl.art'); // or DEFAULT_HUB_URL
const sub = transport.subscribe('hub.pair.v2.<pairId>', (data) => {
// data is a Uint8Array
});
await transport.publish('hub.pair.v2.<pairId>', encodedBytes);
sub.unsubscribe();
await transport.disconnect();
connect(url, opts?) 接受每个连接的验证器。 针对生产超级集群(默认的ACLs),你必须提供一种——要么是静态的 authenticator 或,对于长期会期,a getAuthenticator 提供商在每次重联时重命名证书. MockTransport (进口自 @eternl/hub-core/testing) 执行相同的 Transport 用于测试和记录的界面 publishedMessages。 。 。 .
信件
import {
MessageType,
createApiCall,
isApiResponse,
encodeMessage,
decodeMessage,
generateNonce,
} from '@eternl/hub-core';
const decoded = decodeMessage(bytes);
if (isApiResponse(decoded)) {
// handle the response
}
模块导出已键入信件界面 MessageType 数值映射, is… 类型警卫,和 create… 工厂createConnectRequest, (中文(简体) ), createApiCall, (中文(简体) ), createHeartbeat, (中文(简体) ), createPresencePing(见A/C.5/49/70,附件)。 这个 create… 工厂使用非ce生成器;在本包中,非ce由 generateNonce(sessionId)。 。 。 .
| 信件类型 | 说明 |
|---|---|
CONNECT_REQUEST 编号 : CONNECT_RESPONSE | DAPP 请求连接; 钱包批准/ 拒绝 |
API_CALL 编号 : API_RESPONSE | DAPP 调用 CIP- 30/ CIP- 95 方法; 钱包返回结果 |
ACCESS_GRANTED 编号 : ACCESS_DENIED | 按要求作出的访问决定 |
ACCOUNT_SWITCH 编号 : ACCOUNT_CHANGED | 更改账户请求/通知 |
DISCONNECT 编号 : PAIRING_REMOVED | 临时断开/ 永久断开 |
HEARTBEAT | 保持存活状态 |
PRESENCE 编号 : PRESENCE_PING 编号 : PRESENCE_PONG | 存在状态和往返探测 |
类型
import type { AccountInfo, NetworkType } from '@eternl/hub-core';
import { Permission } from '@eternl/hub-core';
const network: NetworkType = 'mainnet';
const permissions = [
Permission.ADDRESS_READ, // 'address_read'
Permission.SIGN_TX, // 'sign_tx'
];
Permission 包括CIP-30核心方法(NETWORK_ID, (中文(简体) ), ADDRESS_READ, (中文(简体) ), SIGN_TX, (中文(简体) ), SUBMIT_TX(......)和CIP-95治理(GOVERNANCE, (中文(简体) ), DREP_KEY, (中文(简体) ), STAKE_KEYS) (中文(简体) ).
错误
import { EternlConnectError, ErrorCode } from '@eternl/hub-core';
try {
// ... operation
} catch (error) {
if (error instanceof EternlConnectError) {
console.log(error.code); // ErrorCode enum value
console.log(error.recoverable); // boolean — whether a retry may succeed
console.log(error.cause); // original error, if wrapping one
}
}
EternlConnectError 是整个软件包中使用的单一错误类型。 它携带一个 ErrorCode (例如:. TIMEOUT, (中文(简体) ), DECRYPTION_FAILED, (中文(简体) ), REPLAY_DETECTED, (中文(简体) ), HUB_UNAVAILABLE页:1 recoverable 旗帜和可选 cause静态帮助者(EternlConnectError.timeout, (中文(简体) ), .decryptionFailed, (中文(简体) ), .replayDetected,.)构建常见案例.
常数
import {
PROTOCOL_VERSION,
DEFAULT_HUB_URL,
SESSION_EXPIRY_MS,
RECONNECTION,
NONCE_WINDOW_MS,
CHANNELS, // CHANNELS.PAIR_V2(pairId) === `hub.pair.v2.${pairId}`
} from '@eternl/hub-core';
DEFAULT_HUB_URL 是 Eternl 参考 NATS WebSocket 端点, 目前 wss://ws.hub.de-5.eternl.art。通过将不同的 URL 传递到 transport.connect(url)。 。 。 .
安保模式
包裹的中央安全财产是 耐MITM配对 在两个设备之间共享一个简短的PIN。 设计(见 src/crypto/index.ts将四种机制结合起来:
-
ECDH P-256 + AES-256-GCM. 欧洲人大常委会. 每方生成一对电平钥匙,交换公用钥匙,并通过ECDH获得共同的秘密. 框架由AES-256-GCM(认证加密)封存.
-
PIN 键导出 . 会话密钥是
HKDF-SHA256(sharedSecret, salt = SHA-256(utf8(pin) || pairingId), info = "eternl-hub-v2")。 。 。 . 将盐装入PIN意味着攻击者可以代替公用钥匙,但确实 没有 了解PIN不能获得相同的会话密钥,即使完全了解他们谈判的共同秘密. -
SAS6( Short 认证字符串). 6位数的代码来源于共享密语 + PIN 绑定的盐 在一个独特的 HKDF 下
info标签。 双同位素显示,用户确认匹配出带. 如果一个MITM替换出一个或两个公用密钥,那么每一边都会得到不同的共享密钥,所以两个SAS6代码存在分歧,用户在任何签名流量流之前都会中止. -
每帧 AAD + 重放/非保护. 每个 AEAD 框架的额外认证数据是
utf8(pairingId) || SHA-256(pin)[:8] || be64(seq)。使用不同的 PIN 或配对Id 的对等点会在每个框架上失败 AES-GCM 认证。 滑动窗口非ce验证符( E)createNonceValidator窗口 =NONCE_WINDOW_MS拒绝重复播放或陈旧的电文,以及createPairSession拒绝协议版本不匹配的帧PROTOCOL_VERSION。 。 。 .
seq aAD和电线信封中都装着一个每个方向的单调计数器。 注意严格 命令执行被推迟到 SDK 层 —— 说 hub-core 验证非ce窗口和 AAD seq 匹配密码文本,但本身并不拒绝超出再播放检测的订单外发送.
许可证
商业来源许可证1.1(BUSL-1.1) (中文(简体) ). 见 自由主义。 。 。 .
许可是Tastenkunst GmbH。 额外使用赠款 : None - 你可以复制、修改和制作 非生产 使用此软件包。 生产/商业用途需要从塔斯滕昆斯特获得单独的许可证。 在"更改日期"(每个版本首次出版的四周年)上,该版本会转换为GPL-2.0-或-后期.