@arxiumlabs/sdk 0.8.0 → 0.13.1

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/dist/rpc.d.ts CHANGED
@@ -164,6 +164,90 @@ export type Validator = {
164
164
  bls_registered: boolean;
165
165
  operator: string | null;
166
166
  };
167
+ /**
168
+ * One snapshot holder's part in a corporate action (`DistributeToHolders`, `RedeemHolders`, `SplitAsset`). `amount` is in
169
+ * `payout_asset` for a distribution, else in `asset`; for `withheld` it is the share the issuer kept because the holder failed
170
+ * the payout asset's compliance. Floor-rounding dust never appears on a line.
171
+ */
172
+ export type CorporateActionLine = {
173
+ action_index: number;
174
+ signature: string;
175
+ asset: string;
176
+ payout_asset: string | null;
177
+ holder: string;
178
+ amount: bigint;
179
+ outcome: "paid" | "withheld" | "minted" | "forced_back" | "redeemed";
180
+ };
181
+ /** `rpc.simulateModule`: hex strings throughout; `balances` are what the call leaves (`asset` is empty for the native token). */
182
+ export type ModuleSimulation = {
183
+ ok: true;
184
+ gas_used: number;
185
+ return: string;
186
+ writes: {
187
+ key: string;
188
+ value: string | null;
189
+ }[];
190
+ events: {
191
+ topic: string;
192
+ data: string;
193
+ }[];
194
+ balances: {
195
+ asset: string;
196
+ holder: string;
197
+ balance: string;
198
+ }[];
199
+ } | {
200
+ ok: false;
201
+ error: string;
202
+ };
203
+ /** `rpc.module(address)`: `code_hash` is hex, `code_size` in bytes. */
204
+ export type ModuleInfo = {
205
+ address: string;
206
+ code_hash: string;
207
+ deployer: string;
208
+ deployed_at: number;
209
+ version: number;
210
+ code_size: number;
211
+ };
212
+ /** A collateral pledge as stored (`xc_primitives::Pledge`). `asset` is `"Native"` or `{ Asset: ref }`, the node's JSON for `SpendAsset`. */
213
+ export type Pledge = {
214
+ pledgor: string;
215
+ beneficiary: string;
216
+ valuation_agent: string;
217
+ asset: "Native" | {
218
+ Asset: string;
219
+ };
220
+ amount: bigint;
221
+ haircut_bps: number;
222
+ exposure: bigint;
223
+ unit_price: bigint;
224
+ marked_at: number;
225
+ cure_period_blocks: number;
226
+ maturity: number | null;
227
+ margin_call: {
228
+ opened_at: number;
229
+ deadline: number;
230
+ } | null;
231
+ };
232
+ /** `rpc.pledge(id)`: the record plus what it means at the tip. `collateral_value` and `shortfall` are `null` when the valuation overflows. */
233
+ export type PledgeStatus = Pledge & {
234
+ id: string;
235
+ collateral_value: bigint | null;
236
+ shortfall: bigint | null;
237
+ in_default: boolean;
238
+ };
239
+ /** A pledge a block opened or changed (`record` is its end-of-block state), or closed (`record: null`, released or seized). */
240
+ export type PledgeEffect = {
241
+ pledge: string;
242
+ record: Pledge | null;
243
+ };
244
+ /** `rpc.blockEffects(height)`. `corporate_actions` and `pledges` are typed; the other fields mirror the node's `BlockEffects`. */
245
+ export type BlockEffects = {
246
+ height: number;
247
+ corporate_actions: CorporateActionLine[];
248
+ pledges: PledgeEffect[];
249
+ [field: string]: unknown;
250
+ };
167
251
  export type ActionFeeInfo = {
168
252
  action_fee: bigint;
169
253
  weight_fee: bigint;
@@ -186,6 +270,8 @@ export declare class ArxiumRpc {
186
270
  private readonly senders;
187
271
  constructor(options: RpcOptions);
188
272
  private request;
273
+ /** Follows `next_cursor` through a paged list route (`{ items, next_cursor }`); a node that predates paging answers with the whole bare array. */
274
+ private allPages;
189
275
  private genesis?;
190
276
  /** This chain's genesis hash, fetched once per client; every signature binds it. */
191
277
  genesisHash(): Promise<Uint8Array>;
@@ -214,9 +300,36 @@ export declare class ArxiumRpc {
214
300
  blocks<T = unknown>(from: number, to: number): Promise<T>;
215
301
  block<T = unknown>(height: number): Promise<T>;
216
302
  blockByHash<T = unknown>(hash: string): Promise<T>;
217
- /** State changes a block made (balances, assets, stake), for indexers. */
218
- blockEffects<T = unknown>(height: number): Promise<T>;
219
- effects<T = unknown>(from: number, to: number): Promise<T>;
303
+ /** State changes a block made (balances, assets, stake, per-holder corporate-action lines), for indexers. */
304
+ blockEffects<T = BlockEffects>(height: number): Promise<T>;
305
+ effects<T = BlockEffects[]>(from: number, to: number): Promise<T>;
306
+ /** A deployed WASM module's record (`GET /modules/{address}`). */
307
+ module(address: string): Promise<ModuleInfo>;
308
+ /** The module's code as hex. */
309
+ moduleCode(address: string): Promise<{
310
+ code_hash: string;
311
+ code: string;
312
+ }>;
313
+ /** One row of a module's storage by hex key (at most 64 bytes); `value` is hex, or `null` when the row is absent. */
314
+ moduleState(address: string, keyHex: string): Promise<{
315
+ key: string;
316
+ value: string | null;
317
+ }>;
318
+ /**
319
+ * Dry-runs a module call at the tip and returns what it would do; nothing is written. A revert, trap,
320
+ * out-of-gas or refused rule comes back as `{ ok: false, error }`, not as an exception. It skips the action's own
321
+ * checks (signature, nonce, fee, account policy), so a simulation can succeed where the real action is refused.
322
+ * `caller` need not exist on chain. Runs at the next height with the wall clock as its time.
323
+ */
324
+ simulateModule(address: string, call: {
325
+ caller: string;
326
+ input?: Uint8Array;
327
+ gasLimit: number | bigint;
328
+ }): Promise<ModuleSimulation>;
329
+ /** One pledge by id (`pledgeId(pledgor, nonce)`). */
330
+ pledge(id: string): Promise<PledgeStatus>;
331
+ /** The pledges encumbering `address`'s balance of `asset`, oldest first. Native collateral is escrowed, so it is not listed. */
332
+ accountAssetPledges(address: string, asset: string): Promise<PledgeStatus[]>;
220
333
  /** Sorted validator addresses, at the tip or at `height`. */
221
334
  validators<T = string[]>(height?: number): Promise<T>;
222
335
  validator<T = Validator>(address: string): Promise<T>;
package/dist/rpc.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { signAction, signingBytes, submitBody, verifyMultisig } from "./actions.js";
2
2
  import { decodeAddress, isMultisigAddress } from "./bech32.js";
3
- import { asBuffer, fromHex } from "./bincode.js";
3
+ import { asBuffer, fromHex, toHex } from "./bincode.js";
4
4
  import { verifyPolicySignature } from "./policy.js";
5
5
  export class RpcError extends Error {
6
6
  status;
@@ -20,7 +20,7 @@ export class ActionDroppedError extends Error {
20
20
  }
21
21
  }
22
22
  /** Keys whose integer values are token amounts (u128 on the node): always `bigint`, so callers never see a rounded balance. */
23
- const AMOUNT_KEYS = new Set(["balance", "amount", "stake", "active_amount", "frozen_amount", "total_supply", "max_supply", "max_balance_per_holder", "total", "action_fee", "weight_fee", "token_create_fee", "min_stake"]);
23
+ const AMOUNT_KEYS = new Set(["balance", "amount", "stake", "active_amount", "frozen_amount", "total_supply", "max_supply", "max_balance_per_holder", "total", "action_fee", "weight_fee", "token_create_fee", "min_stake", "exposure", "unit_price", "collateral_value", "shortfall"]);
24
24
  const MARK = "\u0000int:";
25
25
  /**
26
26
  * `JSON.parse` that never rounds an integer: the node writes u128 amounts as bare JSON numbers, and anything above 2^53
@@ -46,6 +46,18 @@ export class ArxiumRpc {
46
46
  constructor(options) { this.rpc = options.rpc.replace(/\/+$/, ""); this.token = options.token; this.requestFetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init)); } // browsers and Workers throw "Illegal invocation" if fetch is called with `this` = ArxiumRpc
47
47
  async request(path, init) { const response = await this.requestFetch(`${this.rpc}${path}`, { ...init, headers: { ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}), ...init?.headers } }); const text = await response.text(); if (!response.ok)
48
48
  throw new RpcError(response.status, text || `${response.status} ${response.statusText}`); return text ? parseJson(text) : undefined; }
49
+ /** Follows `next_cursor` through a paged list route (`{ items, next_cursor }`); a node that predates paging answers with the whole bare array. */
50
+ async allPages(path) {
51
+ const items = [];
52
+ for (let cursor = 0; cursor !== null;) {
53
+ const page = await this.request(`${path}${path.includes("?") ? "&" : "?"}limit=100&cursor=${cursor}`);
54
+ if (Array.isArray(page))
55
+ return page;
56
+ items.push(...page.items);
57
+ cursor = page.next_cursor;
58
+ }
59
+ return items;
60
+ }
49
61
  genesis;
50
62
  /** This chain's genesis hash, fetched once per client; every signature binds it. */
51
63
  genesisHash() { return this.genesis ??= this.request("/genesis-hash").then((r) => fromHex(r.genesis_hash.replace(/^0x/, "")), (e) => { this.genesis = undefined; throw e; }); }
@@ -61,7 +73,7 @@ export class ArxiumRpc {
61
73
  delegatedStake(master, validator) { return this.request(`/stake/${encodeURIComponent(master)}/${encodeURIComponent(validator)}`); }
62
74
  blsKey(address) { return this.request(`/accounts/${encodeURIComponent(address)}/bls-key`); }
63
75
  /** Every registered asset, or one issuer's. */
64
- listAssets(issuer) { return this.request(issuer ? `/assets?issuer=${encodeURIComponent(issuer)}` : "/assets"); }
76
+ listAssets(issuer) { return this.allPages(issuer ? `/assets?issuer=${encodeURIComponent(issuer)}` : "/assets"); }
65
77
  asset(assetRef) { return this.request(`/assets/${encodeURIComponent(assetRef)}`); }
66
78
  /** The asset `issuer` registered as `assetId`, or `null` when that id is still free for them. */
67
79
  async assetByAlias(issuer, assetId) { try {
@@ -73,8 +85,8 @@ export class ArxiumRpc {
73
85
  throw error;
74
86
  } }
75
87
  /** The cap table: every address with a non-zero balance, with its freeze state. */
76
- assetHolders(assetRef) { return this.request(`/assets/${encodeURIComponent(assetRef)}/holders`); }
77
- attestors() { return this.request("/attestors"); }
88
+ assetHolders(assetRef) { return this.allPages(`/assets/${encodeURIComponent(assetRef)}/holders`); }
89
+ attestors() { return this.allPages("/attestors"); }
78
90
  attestor(address) { return this.request(`/attestors/${encodeURIComponent(address)}`); }
79
91
  submit(action) { return this.request("/actions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(action) }); }
80
92
  async action(signature) { try {
@@ -88,9 +100,31 @@ export class ArxiumRpc {
88
100
  blocks(from, to) { return this.request(`/blocks?from=${from}&to=${to}`); }
89
101
  block(height) { return this.request(`/blocks/${height}`); }
90
102
  blockByHash(hash) { return this.request(`/blocks/by-hash/${encodeURIComponent(hash)}`); }
91
- /** State changes a block made (balances, assets, stake), for indexers. */
103
+ /** State changes a block made (balances, assets, stake, per-holder corporate-action lines), for indexers. */
92
104
  blockEffects(height) { return this.request(`/blocks/${height}/effects`); }
93
105
  effects(from, to) { return this.request(`/effects?from=${from}&to=${to}`); }
106
+ /** A deployed WASM module's record (`GET /modules/{address}`). */
107
+ module(address) { return this.request(`/modules/${encodeURIComponent(address)}`); }
108
+ /** The module's code as hex. */
109
+ moduleCode(address) { return this.request(`/modules/${encodeURIComponent(address)}/code`); }
110
+ /** One row of a module's storage by hex key (at most 64 bytes); `value` is hex, or `null` when the row is absent. */
111
+ moduleState(address, keyHex) { return this.request(`/modules/${encodeURIComponent(address)}/state/${encodeURIComponent(keyHex)}`); }
112
+ /**
113
+ * Dry-runs a module call at the tip and returns what it would do; nothing is written. A revert, trap,
114
+ * out-of-gas or refused rule comes back as `{ ok: false, error }`, not as an exception. It skips the action's own
115
+ * checks (signature, nonce, fee, account policy), so a simulation can succeed where the real action is refused.
116
+ * `caller` need not exist on chain. Runs at the next height with the wall clock as its time.
117
+ */
118
+ simulateModule(address, call) {
119
+ const gas = Number(call.gasLimit);
120
+ if (!Number.isSafeInteger(gas) || gas < 0)
121
+ throw new RangeError("gasLimit must be a non-negative safe integer");
122
+ return this.request(`/modules/${encodeURIComponent(address)}/simulate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ caller: call.caller, input: toHex(call.input ?? new Uint8Array()), gas_limit: gas }) });
123
+ }
124
+ /** One pledge by id (`pledgeId(pledgor, nonce)`). */
125
+ pledge(id) { return this.request(`/pledges/${encodeURIComponent(id)}`); }
126
+ /** The pledges encumbering `address`'s balance of `asset`, oldest first. Native collateral is escrowed, so it is not listed. */
127
+ accountAssetPledges(address, asset) { return this.request(`/accounts/${encodeURIComponent(address)}/assets/${encodeURIComponent(asset)}/pledges`); }
94
128
  /** Sorted validator addresses, at the tip or at `height`. */
95
129
  validators(height) { return this.request(height === undefined ? "/validators" : `/validators?height=${height}`); }
96
130
  validator(address) { return this.request(`/validators/${encodeURIComponent(address)}`); }
@@ -99,7 +133,7 @@ export class ArxiumRpc {
99
133
  operatorValidators(operator) { return this.request(`/operators/${encodeURIComponent(operator)}/validators`); }
100
134
  finality() { return this.request("/finality"); }
101
135
  /** Ids of stored equivocation/fault evidence; `evidence(id)` returns one. */
102
- evidenceList() { return this.request("/evidence"); }
136
+ evidenceList() { return this.allPages("/evidence"); }
103
137
  evidence(id) { return this.request(`/evidence/${encodeURIComponent(id)}`); }
104
138
  /** Merkle proofs against a finalized state root, for light clients. */
105
139
  accountProof(address) { return this.request(`/accounts/${encodeURIComponent(address)}/proof`); }
@@ -112,12 +146,15 @@ export class ArxiumRpc {
112
146
  async feeParams() { const fee = await this.actionFee(); return { actionFee: BigInt(fee.action_fee), weightFee: BigInt(fee.weight_fee) }; }
113
147
  /** Signs at the sender's current nonce, submits and polls until confirmed (or finalized). Calls for one sender run one after another. */
114
148
  sendAction(options) {
149
+ // Capture at invocation, including time spent waiting in this sender's queue.
150
+ options = { ...options, payload: Uint8Array.from(options.payload) };
115
151
  const previous = this.senders.get(options.sender) ?? Promise.resolve();
116
- const run = previous.catch(() => undefined).then(() => this.sendNow(options));
117
- const tail = run.catch(() => undefined);
118
- this.senders.set(options.sender, tail);
119
- void tail.then(() => { if (this.senders.get(options.sender) === tail)
120
- this.senders.delete(options.sender); });
152
+ // A failed/uncertain send rejects already queued requests instead of signing again.
153
+ const run = previous.then(() => this.sendNow(options));
154
+ this.senders.set(options.sender, run);
155
+ const cleanup = () => { if (this.senders.get(options.sender) === run)
156
+ this.senders.delete(options.sender); };
157
+ void run.then(cleanup, cleanup);
121
158
  return run;
122
159
  }
123
160
  async sendNow(options) {
@@ -0,0 +1,63 @@
1
+ import { type DvpOrder } from "./actions.js";
2
+ /** Pure Ed25519 over the supplied message bytes, not Ed25519ph or a provider digest mode. */
3
+ export interface Ed25519Signer {
4
+ readonly algorithm: "Ed25519";
5
+ /** Raw32-byte compressed Ed25519 public key, not DER, PEM or an address. */
6
+ readonly publicKey: Uint8Array;
7
+ /** One attempt. Return the raw64-byte signature; reject on refusal or an uncertain outcome. */
8
+ sign(message: Uint8Array): Promise<Uint8Array>;
9
+ }
10
+ export declare class SignerVerificationError extends Error {
11
+ constructor(message: string);
12
+ }
13
+ /** Local developer adapter. Does not export private key material or perform a probe signature. */
14
+ export declare function localEd25519Signer(privateKey: CryptoKey, publicKey: Uint8Array): Ed25519Signer;
15
+ /** Sign exactly these bytes, verify against the caller-selected key, return lowercase signature hex. */
16
+ export declare function signBytesWithSigner(signer: Ed25519Signer, expectedPublicKey: Uint8Array, message: Uint8Array): Promise<string>;
17
+ /** Pins the key and callback at creation; plugs into existing ArxiumRpc.sendAction({ sign }). */
18
+ export declare function verifiedSignerCallback(signer: Ed25519Signer, expectedPublicKey: Uint8Array): (message: Uint8Array) => Promise<string>;
19
+ /** The expected key may be a member of a multisig/policy sender; authorization is a separate check. */
20
+ export declare function signActionWithSigner(signer: Ed25519Signer, expectedPublicKey: Uint8Array, genesis: Uint8Array, sender: string, nonce: number | bigint, payload: Uint8Array): Promise<string>;
21
+ export declare function signDvpConsentWithSigner(signer: Ed25519Signer, expectedPublicKey: Uint8Array, genesis: Uint8Array, initiator: string, order: DvpOrder, counterpartyNonce: number | bigint): Promise<string>;
22
+ /**
23
+ * The two methods of `KMS` from `@aws-sdk/client-kms` (the aggregated client, not `KMSClient`) this adapter calls.
24
+ * The SDK ships no AWS dependency: pass your own configured client.
25
+ */
26
+ export type AwsKmsClient = {
27
+ getPublicKey(input: {
28
+ KeyId: string;
29
+ }): Promise<{
30
+ PublicKey?: Uint8Array;
31
+ KeySpec?: string;
32
+ }>;
33
+ sign(input: {
34
+ KeyId: string;
35
+ Message: Uint8Array;
36
+ MessageType: "RAW";
37
+ SigningAlgorithm: "ED25519_SHA_512";
38
+ }): Promise<{
39
+ Signature?: Uint8Array;
40
+ }>;
41
+ };
42
+ /** AWS KMS caps a RAW message at 4096 bytes; pure Ed25519 cannot be prehashed, so a larger action cannot be KMS-signed. */
43
+ export declare const AWS_KMS_MAX_MESSAGE = 4096;
44
+ /** An `ECC_NIST_EDWARDS25519` key with `ED25519_SHA_512` (pure Ed25519, `MessageType: RAW`). Reads the key's public key once. */
45
+ export declare function awsKmsEd25519Signer(kms: AwsKmsClient, keyId: string): Promise<Ed25519Signer>;
46
+ /** The subset of a `pkcs11js` `PKCS11` instance this adapter calls. The SDK ships no native module: pass your own, logged in. */
47
+ export type Pkcs11Module = {
48
+ C_SignInit(session: Buffer, mechanism: {
49
+ mechanism: number;
50
+ }, key: Buffer): void;
51
+ C_Sign(session: Buffer, data: Buffer, signature: Buffer): Buffer;
52
+ C_GetAttributeValue(session: Buffer, object: Buffer, template: {
53
+ type: number;
54
+ }[]): {
55
+ type: number;
56
+ value?: Buffer;
57
+ }[];
58
+ };
59
+ /**
60
+ * A `CKK_EC_EDWARDS` key pair on a PKCS#11 v3 token (YubiHSM 2, Luna 7.7+, SoftHSM 2.6+), signed with `CKM_EDDSA` and no
61
+ * parameters: pure Ed25519. The public key comes from the public object's `CKA_EC_POINT`.
62
+ */
63
+ export declare function pkcs11Ed25519Signer(pkcs11: Pkcs11Module, session: Buffer, privateKey: Buffer, publicKeyObject: Buffer): Ed25519Signer;
package/dist/signer.js ADDED
@@ -0,0 +1,107 @@
1
+ import { dvpConsentBytes, signingBytes } from "./actions.js";
2
+ import { asBuffer, toHex } from "./bincode.js";
3
+ export class SignerVerificationError extends Error {
4
+ constructor(message) { super(message); this.name = "SignerVerificationError"; }
5
+ }
6
+ function rawKey(value) {
7
+ if (!(value instanceof Uint8Array) || value.length !== 32)
8
+ throw new SignerVerificationError("expected a raw32-byte Ed25519 public key");
9
+ return Uint8Array.from(value);
10
+ }
11
+ function pinSigner(signer, expectedPublicKey) {
12
+ const expected = rawKey(expectedPublicKey), advertised = rawKey(signer.publicKey);
13
+ const sign = signer.sign;
14
+ if (signer.algorithm !== "Ed25519" || typeof sign !== "function")
15
+ throw new SignerVerificationError("signer must implement pure Ed25519");
16
+ if (!expected.every((byte, i) => byte === advertised[i]))
17
+ throw new SignerVerificationError("signer public key does not match the expected key");
18
+ return { publicKey: expected, sign: sign.bind(signer) };
19
+ }
20
+ async function verifiedSign(signer, message) {
21
+ // Keep the trusted message private. A provider may retain/mutate its own input buffer.
22
+ if (!(message instanceof Uint8Array))
23
+ throw new SignerVerificationError("message must be Uint8Array bytes");
24
+ const snapshot = Uint8Array.from(message);
25
+ const response = await signer.sign(Uint8Array.from(snapshot));
26
+ if (!(response instanceof Uint8Array) || response.length !== 64)
27
+ throw new SignerVerificationError("signer must return a raw64-byte Ed25519 signature");
28
+ const signature = Uint8Array.from(response);
29
+ const key = await crypto.subtle.importKey("raw", asBuffer(signer.publicKey), "Ed25519", false, ["verify"]);
30
+ if (!await crypto.subtle.verify("Ed25519", key, asBuffer(signature), asBuffer(snapshot)))
31
+ throw new SignerVerificationError("signature does not verify for the expected key and original message");
32
+ return toHex(signature);
33
+ }
34
+ /** Local developer adapter. Does not export private key material or perform a probe signature. */
35
+ export function localEd25519Signer(privateKey, publicKey) {
36
+ const raw = rawKey(publicKey);
37
+ if (privateKey.type !== "private" || privateKey.algorithm.name !== "Ed25519" || !privateKey.usages.includes("sign"))
38
+ throw new SignerVerificationError("local signer needs an Ed25519 private signing key");
39
+ return Object.freeze({
40
+ algorithm: "Ed25519",
41
+ get publicKey() { return Uint8Array.from(raw); },
42
+ async sign(message) { return new Uint8Array(await crypto.subtle.sign("Ed25519", privateKey, asBuffer(message))); },
43
+ });
44
+ }
45
+ /** Sign exactly these bytes, verify against the caller-selected key, return lowercase signature hex. */
46
+ export async function signBytesWithSigner(signer, expectedPublicKey, message) {
47
+ return verifiedSign(pinSigner(signer, expectedPublicKey), message);
48
+ }
49
+ /** Pins the key and callback at creation; plugs into existing ArxiumRpc.sendAction({ sign }). */
50
+ export function verifiedSignerCallback(signer, expectedPublicKey) {
51
+ const pinned = pinSigner(signer, expectedPublicKey);
52
+ return (message) => verifiedSign(pinned, message);
53
+ }
54
+ /** The expected key may be a member of a multisig/policy sender; authorization is a separate check. */
55
+ export async function signActionWithSigner(signer, expectedPublicKey, genesis, sender, nonce, payload) {
56
+ return signBytesWithSigner(signer, expectedPublicKey, signingBytes(genesis, sender, nonce, payload));
57
+ }
58
+ export async function signDvpConsentWithSigner(signer, expectedPublicKey, genesis, initiator, order, counterpartyNonce) {
59
+ return signBytesWithSigner(signer, expectedPublicKey, dvpConsentBytes(genesis, initiator, order, counterpartyNonce));
60
+ }
61
+ const SPKI_ED25519_PREFIX = "302a300506032b6570032100";
62
+ /** Raw 32-byte key from an Ed25519 SubjectPublicKeyInfo (what AWS KMS GetPublicKey returns). */
63
+ function spkiEd25519(der) {
64
+ if (!(der instanceof Uint8Array) || der.length !== 44 || toHex(der.subarray(0, 12)) !== SPKI_ED25519_PREFIX)
65
+ throw new SignerVerificationError("expected an Ed25519 SubjectPublicKeyInfo public key");
66
+ return der.slice(12);
67
+ }
68
+ /** AWS KMS caps a RAW message at 4096 bytes; pure Ed25519 cannot be prehashed, so a larger action cannot be KMS-signed. */
69
+ export const AWS_KMS_MAX_MESSAGE = 4096;
70
+ /** An `ECC_NIST_EDWARDS25519` key with `ED25519_SHA_512` (pure Ed25519, `MessageType: RAW`). Reads the key's public key once. */
71
+ export async function awsKmsEd25519Signer(kms, keyId) {
72
+ const info = await kms.getPublicKey({ KeyId: keyId });
73
+ if (info.KeySpec !== "ECC_NIST_EDWARDS25519")
74
+ throw new SignerVerificationError(`KMS key ${keyId} is ${info.KeySpec}, not ECC_NIST_EDWARDS25519`);
75
+ const raw = spkiEd25519(info.PublicKey);
76
+ return Object.freeze({
77
+ algorithm: "Ed25519",
78
+ get publicKey() { return Uint8Array.from(raw); },
79
+ async sign(message) {
80
+ if (message.length > AWS_KMS_MAX_MESSAGE)
81
+ throw new SignerVerificationError(`message is ${message.length} bytes; AWS KMS signs at most ${AWS_KMS_MAX_MESSAGE}`);
82
+ const { Signature } = await kms.sign({ KeyId: keyId, Message: message, MessageType: "RAW", SigningAlgorithm: "ED25519_SHA_512" });
83
+ if (!(Signature instanceof Uint8Array))
84
+ throw new SignerVerificationError("KMS returned no signature");
85
+ return Signature;
86
+ },
87
+ });
88
+ }
89
+ const CKM_EDDSA = 0x1057, CKA_EC_POINT = 0x181;
90
+ /**
91
+ * A `CKK_EC_EDWARDS` key pair on a PKCS#11 v3 token (YubiHSM 2, Luna 7.7+, SoftHSM 2.6+), signed with `CKM_EDDSA` and no
92
+ * parameters: pure Ed25519. The public key comes from the public object's `CKA_EC_POINT`.
93
+ */
94
+ export function pkcs11Ed25519Signer(pkcs11, session, privateKey, publicKeyObject) {
95
+ const point = pkcs11.C_GetAttributeValue(session, publicKeyObject, [{ type: CKA_EC_POINT }])[0]?.value;
96
+ // PKCS#11 3.0 wraps the point in a DER OCTET STRING; some tokens return it bare.
97
+ const raw = rawKey(point?.length === 34 && point[0] === 0x04 && point[1] === 0x20 ? point.subarray(2) : point);
98
+ return Object.freeze({
99
+ algorithm: "Ed25519",
100
+ get publicKey() { return Uint8Array.from(raw); },
101
+ async sign(message) {
102
+ // ponytail: init+sign run synchronously back to back, so concurrent calls never interleave on one session.
103
+ pkcs11.C_SignInit(session, { mechanism: CKM_EDDSA }, privateKey);
104
+ return Uint8Array.from(pkcs11.C_Sign(session, Buffer.from(message), Buffer.alloc(64)));
105
+ },
106
+ });
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arxiumlabs/sdk",
3
- "version": "0.8.0",
3
+ "version": "0.13.1",
4
4
  "description": "Arxium TypeScript SDK and arx CLI",
5
5
  "type": "module",
6
6
  "private": false,
@@ -25,7 +25,8 @@
25
25
  "files": [
26
26
  "dist",
27
27
  "!dist/*.test.*",
28
- "README.md"
28
+ "README.md",
29
+ "SIGNERS.md"
29
30
  ],
30
31
  "engines": {
31
32
  "node": ">=20"
@@ -34,7 +35,7 @@
34
35
  "build": "tsc -p tsconfig.json",
35
36
  "check:package": "node scripts/release-check.mjs",
36
37
  "test:release": "node --test scripts/release-check.test.mjs",
37
- "test": "npm run build && node dist/codec.test.js && node dist/policy.test.js && node dist/cli.test.js"
38
+ "test": "npm run build && node dist/signer.test.js && node dist/codec.test.js && node dist/policy.test.js && node dist/cli.test.js"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/node": "^20.0.0",