@zga/client 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/billing.js +1 -1
- package/dist/explorer.js +8 -1
- package/dist/idl.js +8 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/observer.d.ts +4 -1
- package/dist/observer.js +2 -0
- package/dist/onion.d.ts +42 -0
- package/dist/onion.js +159 -0
- package/dist/settlement.js +1 -1
- package/package.json +4 -1
package/dist/billing.js
CHANGED
|
@@ -89,7 +89,7 @@ async function billSubscription(env, receipt, guildSignature, attestations) {
|
|
|
89
89
|
zgapReserve: addr.zgapReserve,
|
|
90
90
|
zgapReserveZgaAccount: (0, token_1.associatedTokenAddress)(zgaMint, addr.zgapReserve),
|
|
91
91
|
lpLockTokenAccount: addr.lpLock,
|
|
92
|
-
guildTokenAccount: (0, token_1.associatedTokenAddress)(addr.zgapMint, guildAccount.
|
|
92
|
+
guildTokenAccount: (0, token_1.associatedTokenAddress)(addr.zgapMint, guildAccount.authority),
|
|
93
93
|
gameManifest: addr.gameManifest(gameId),
|
|
94
94
|
gameCreatorAccount: addr.gameCreator(creator, gameId),
|
|
95
95
|
royaltyVault: addr.royaltyVault(creator, gameId),
|
package/dist/explorer.js
CHANGED
|
@@ -26,16 +26,23 @@ async function read(url, max, opts) {
|
|
|
26
26
|
throw new NetworkError(`refused: ${err.message}`);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
const pinned = opts.guard?.fetch;
|
|
29
30
|
let res;
|
|
30
31
|
try {
|
|
31
|
-
res =
|
|
32
|
+
res = pinned
|
|
33
|
+
? await pinned(target, { signal: AbortSignal.timeout(opts.timeoutMs ?? 20000), maxBytes: max })
|
|
34
|
+
: await fetch(target, { redirect: 'error', signal: AbortSignal.timeout(opts.timeoutMs ?? 20000) });
|
|
32
35
|
}
|
|
33
36
|
catch (err) {
|
|
34
37
|
const why = String(err?.cause?.message ?? err.message);
|
|
35
38
|
if (/redirect/i.test(why))
|
|
36
39
|
throw new Error('the host redirects');
|
|
40
|
+
if (pinned && /larger than the limit/.test(why))
|
|
41
|
+
throw new Error(`larger than ${max} bytes`);
|
|
37
42
|
throw new NetworkError(why);
|
|
38
43
|
}
|
|
44
|
+
if (pinned && res.status >= 300 && res.status < 400)
|
|
45
|
+
throw new Error('the host redirects');
|
|
39
46
|
if (!res.ok)
|
|
40
47
|
throw new Error(`HTTP ${res.status}`);
|
|
41
48
|
if (Number(res.headers.get('content-length') ?? 0) > max)
|
package/dist/idl.js
CHANGED
|
@@ -5,8 +5,13 @@ exports.variantIndex = variantIndex;
|
|
|
5
5
|
const FIXED = {
|
|
6
6
|
bool: 1, u8: 1, i8: 1, u16: 2, i16: 2, u32: 4, i32: 4, f32: 4, u64: 8, i64: 8, f64: 8, u128: 16, i128: 16, pubkey: 32,
|
|
7
7
|
};
|
|
8
|
+
// A Program built by Anchor carries its IDL with every name in camelCase (Session becomes session, game_id becomes gameId).
|
|
9
|
+
// These helpers accept either spelling, so they work on that IDL and on the file as written.
|
|
10
|
+
const camel = (s) => s.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase()).replace(/^[A-Z]/, (c) => c.toLowerCase());
|
|
11
|
+
const plain = (s) => s.replace(/_/g, '').toLowerCase();
|
|
8
12
|
function typeDef(idl, name) {
|
|
9
|
-
const
|
|
13
|
+
const wanted = camel(name);
|
|
14
|
+
const def = (idl.types ?? []).find((t) => t.name === name || camel(t.name) === wanted);
|
|
10
15
|
if (!def)
|
|
11
16
|
throw new Error(`The IDL has no type ${name}`);
|
|
12
17
|
return def;
|
|
@@ -37,7 +42,7 @@ function fieldOffset(idl, account, field) {
|
|
|
37
42
|
throw new Error(`${account} is not a struct`);
|
|
38
43
|
let offset = 8;
|
|
39
44
|
for (const f of def.type.fields ?? []) {
|
|
40
|
-
if (f.name === field)
|
|
45
|
+
if (f.name === field || camel(f.name) === camel(field))
|
|
41
46
|
return offset;
|
|
42
47
|
offset += sizeOf(idl, f.type);
|
|
43
48
|
}
|
|
@@ -45,7 +50,7 @@ function fieldOffset(idl, account, field) {
|
|
|
45
50
|
}
|
|
46
51
|
function variantIndex(idl, enumName, variant) {
|
|
47
52
|
const def = typeDef(idl, enumName);
|
|
48
|
-
const index = (def.type.variants ?? []).findIndex((v) => v.name
|
|
53
|
+
const index = (def.type.variants ?? []).findIndex((v) => plain(v.name) === plain(variant));
|
|
49
54
|
if (index < 0)
|
|
50
55
|
throw new Error(`${enumName} has no variant ${variant}`);
|
|
51
56
|
return index;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/observer.d.ts
CHANGED
|
@@ -23,7 +23,10 @@ export declare function availabilityAcceptable(c: {
|
|
|
23
23
|
samples: number;
|
|
24
24
|
up: number;
|
|
25
25
|
}, minCoverage?: number, minUp?: number): boolean;
|
|
26
|
-
export type
|
|
26
|
+
export type PinnedFetch = (url: string, init?: any) => Promise<Response>;
|
|
27
|
+
export type UrlGuard = ((url: string) => Promise<URL>) & {
|
|
28
|
+
fetch?: PinnedFetch;
|
|
29
|
+
};
|
|
27
30
|
export declare function probeGuild(base: string, guard?: UrlGuard): Promise<boolean>;
|
|
28
31
|
export interface PendingReceipt {
|
|
29
32
|
session: string;
|
package/dist/observer.js
CHANGED
|
@@ -114,6 +114,8 @@ function availabilityAcceptable(c, minCoverage = 0.9, minUp = 0.9) {
|
|
|
114
114
|
}
|
|
115
115
|
const anyUrl = async (url) => new URL(url);
|
|
116
116
|
async function guarded(guard, url, init = {}) {
|
|
117
|
+
if (guard.fetch)
|
|
118
|
+
return guard.fetch(url, { signal: AbortSignal.timeout(20000), ...init });
|
|
117
119
|
const target = await guard(url);
|
|
118
120
|
return fetch(target, { redirect: 'manual', signal: AbortSignal.timeout(20000), ...init });
|
|
119
121
|
}
|
package/dist/onion.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export declare const ONION_PACKET_SIZE = 4096;
|
|
2
|
+
export declare const ONION_MIN_HOPS = 3;
|
|
3
|
+
export declare const ONION_MAX_HOPS = 5;
|
|
4
|
+
export declare const ONION_VALID_MINUTES = 10;
|
|
5
|
+
export declare const ONION_IDENTITY_TAG = "zga-nop-onion-key-v1";
|
|
6
|
+
export declare const ONION_LAYER_OVERHEAD: number;
|
|
7
|
+
export declare function onionMaxPayload(hops: number): number;
|
|
8
|
+
export declare function buildOnionPacket(relayKeys: Uint8Array[], payload: Uint8Array, nowMin?: number): Promise<Uint8Array>;
|
|
9
|
+
export interface OnionIdentity {
|
|
10
|
+
operator: string;
|
|
11
|
+
onion_pubkey: string;
|
|
12
|
+
signature: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function onionIdentityMessage(onionPub: Uint8Array, operator: Uint8Array): Uint8Array;
|
|
15
|
+
/** The relay's onion key, if the document is signed by the operator it names and that operator is the one expected. */
|
|
16
|
+
export declare function verifyOnionIdentity(doc: OnionIdentity, expectedOperator: string): Uint8Array | null;
|
|
17
|
+
export interface RelayCandidate {
|
|
18
|
+
operator: string;
|
|
19
|
+
endpoint: string;
|
|
20
|
+
}
|
|
21
|
+
export interface OnionRelay extends RelayCandidate {
|
|
22
|
+
onionKey: Uint8Array;
|
|
23
|
+
}
|
|
24
|
+
type Fetch = (url: string, init?: any) => Promise<{
|
|
25
|
+
ok: boolean;
|
|
26
|
+
status: number;
|
|
27
|
+
json(): Promise<any>;
|
|
28
|
+
}>;
|
|
29
|
+
/** Fetches each candidate's signed onion identity and keeps the ones that verify against the operator the caller took from the chain. */
|
|
30
|
+
export declare function loadOnionRelays(candidates: RelayCandidate[], fetchFn?: Fetch, timeoutMs?: number): Promise<OnionRelay[]>;
|
|
31
|
+
/** A random circuit of distinct relays run by distinct operators. */
|
|
32
|
+
export declare function chooseCircuit(relays: OnionRelay[], hops?: number, random?: () => number): OnionRelay[];
|
|
33
|
+
export interface OnionSendRequest {
|
|
34
|
+
recipient_view_pubkey: string;
|
|
35
|
+
payload_b64: string;
|
|
36
|
+
}
|
|
37
|
+
/** Wraps the request in a circuit and submits it to the first relay. The last relay delivers it as a /stealth/send request. */
|
|
38
|
+
export declare function sendOnion(circuit: OnionRelay[], request: OnionSendRequest, fetchFn?: Fetch): Promise<{
|
|
39
|
+
firstRelay: string;
|
|
40
|
+
status: number;
|
|
41
|
+
}>;
|
|
42
|
+
export {};
|
package/dist/onion.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ONION_LAYER_OVERHEAD = exports.ONION_IDENTITY_TAG = exports.ONION_VALID_MINUTES = exports.ONION_MAX_HOPS = exports.ONION_MIN_HOPS = exports.ONION_PACKET_SIZE = void 0;
|
|
7
|
+
exports.onionMaxPayload = onionMaxPayload;
|
|
8
|
+
exports.buildOnionPacket = buildOnionPacket;
|
|
9
|
+
exports.onionIdentityMessage = onionIdentityMessage;
|
|
10
|
+
exports.verifyOnionIdentity = verifyOnionIdentity;
|
|
11
|
+
exports.loadOnionRelays = loadOnionRelays;
|
|
12
|
+
exports.chooseCircuit = chooseCircuit;
|
|
13
|
+
exports.sendOnion = sendOnion;
|
|
14
|
+
const core_1 = require("@hpke/core");
|
|
15
|
+
const dhkem_x25519_1 = require("@hpke/dhkem-x25519");
|
|
16
|
+
const chacha20poly1305_1 = require("@hpke/chacha20poly1305");
|
|
17
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
18
|
+
const tweetnacl_1 = __importDefault(require("tweetnacl"));
|
|
19
|
+
exports.ONION_PACKET_SIZE = 4096;
|
|
20
|
+
exports.ONION_MIN_HOPS = 3;
|
|
21
|
+
exports.ONION_MAX_HOPS = 5;
|
|
22
|
+
exports.ONION_VALID_MINUTES = 10;
|
|
23
|
+
exports.ONION_IDENTITY_TAG = 'zga-nop-onion-key-v1';
|
|
24
|
+
const ENC_LEN = 32;
|
|
25
|
+
const TAG_LEN = 16;
|
|
26
|
+
const HEADER_LEN = 1 + 4 + 32 + 2;
|
|
27
|
+
exports.ONION_LAYER_OVERHEAD = ENC_LEN + TAG_LEN + HEADER_LEN;
|
|
28
|
+
const FORWARD = 0;
|
|
29
|
+
const DELIVER = 1;
|
|
30
|
+
const INFO = new TextEncoder().encode('zga-context');
|
|
31
|
+
const suite = new core_1.CipherSuite({ kem: new dhkem_x25519_1.DhkemX25519HkdfSha256(), kdf: new core_1.HkdfSha256(), aead: new chacha20poly1305_1.Chacha20Poly1305() });
|
|
32
|
+
const toBuffer = (u8) => u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
|
|
33
|
+
const nowMinutes = () => Math.floor(Date.now() / 60000);
|
|
34
|
+
const isZero = (k) => k.every((b) => b === 0);
|
|
35
|
+
const same = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
36
|
+
function onionMaxPayload(hops) {
|
|
37
|
+
return Math.max(0, exports.ONION_PACKET_SIZE - exports.ONION_LAYER_OVERHEAD * hops - 2);
|
|
38
|
+
}
|
|
39
|
+
async function buildOnionPacket(relayKeys, payload, nowMin = nowMinutes()) {
|
|
40
|
+
const n = relayKeys.length;
|
|
41
|
+
if (n < exports.ONION_MIN_HOPS)
|
|
42
|
+
throw new Error(`An onion circuit needs at least ${exports.ONION_MIN_HOPS} relays`);
|
|
43
|
+
if (n > exports.ONION_MAX_HOPS)
|
|
44
|
+
throw new Error(`An onion circuit takes at most ${exports.ONION_MAX_HOPS} relays`);
|
|
45
|
+
relayKeys.forEach((key, i) => {
|
|
46
|
+
if (key.length !== 32)
|
|
47
|
+
throw new Error('A relay key must be 32 bytes');
|
|
48
|
+
if (isZero(key))
|
|
49
|
+
throw new Error('A relay key must not be all zeros');
|
|
50
|
+
if (relayKeys.slice(0, i).some((other) => same(other, key)))
|
|
51
|
+
throw new Error('Every relay in a circuit must be distinct');
|
|
52
|
+
});
|
|
53
|
+
if (payload.length > onionMaxPayload(n)) {
|
|
54
|
+
throw new Error(`The payload is ${payload.length} bytes and a ${n}-relay circuit carries at most ${onionMaxPayload(n)}`);
|
|
55
|
+
}
|
|
56
|
+
let current = new Uint8Array(exports.ONION_PACKET_SIZE - exports.ONION_LAYER_OVERHEAD * n);
|
|
57
|
+
globalThis.crypto.getRandomValues(current);
|
|
58
|
+
new DataView(current.buffer).setUint16(0, payload.length);
|
|
59
|
+
current.set(payload, 2);
|
|
60
|
+
const validUntil = nowMin + exports.ONION_VALID_MINUTES;
|
|
61
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
62
|
+
const last = i === n - 1;
|
|
63
|
+
const plain = new Uint8Array(HEADER_LEN + current.length);
|
|
64
|
+
const view = new DataView(plain.buffer);
|
|
65
|
+
plain[0] = last ? DELIVER : FORWARD;
|
|
66
|
+
view.setUint32(1, validUntil);
|
|
67
|
+
if (!last)
|
|
68
|
+
plain.set(relayKeys[i + 1], 5);
|
|
69
|
+
view.setUint16(37, current.length);
|
|
70
|
+
plain.set(current, HEADER_LEN);
|
|
71
|
+
const recipientPublicKey = await suite.kem.deserializePublicKey(toBuffer(relayKeys[i]));
|
|
72
|
+
const sender = await suite.createSenderContext({ recipientPublicKey, info: toBuffer(INFO) });
|
|
73
|
+
const ciphertext = new Uint8Array(await sender.seal(toBuffer(plain)));
|
|
74
|
+
const enc = new Uint8Array(sender.enc);
|
|
75
|
+
if (enc.length !== ENC_LEN)
|
|
76
|
+
throw new Error('Unexpected encapsulated key length');
|
|
77
|
+
const layer = new Uint8Array(enc.length + ciphertext.length);
|
|
78
|
+
layer.set(enc, 0);
|
|
79
|
+
layer.set(ciphertext, enc.length);
|
|
80
|
+
current = layer;
|
|
81
|
+
}
|
|
82
|
+
if (current.length !== exports.ONION_PACKET_SIZE)
|
|
83
|
+
throw new Error('The onion packet has the wrong size');
|
|
84
|
+
return current;
|
|
85
|
+
}
|
|
86
|
+
const hexBytes = (hex, length) => {
|
|
87
|
+
if (typeof hex !== 'string' || !/^[0-9a-fA-F]+$/.test(hex) || hex.length !== length * 2)
|
|
88
|
+
return null;
|
|
89
|
+
return Uint8Array.from(Buffer.from(hex, 'hex'));
|
|
90
|
+
};
|
|
91
|
+
function onionIdentityMessage(onionPub, operator) {
|
|
92
|
+
const tag = new TextEncoder().encode(exports.ONION_IDENTITY_TAG);
|
|
93
|
+
const message = new Uint8Array(tag.length + onionPub.length + operator.length);
|
|
94
|
+
message.set(tag, 0);
|
|
95
|
+
message.set(onionPub, tag.length);
|
|
96
|
+
message.set(operator, tag.length + onionPub.length);
|
|
97
|
+
return message;
|
|
98
|
+
}
|
|
99
|
+
/** The relay's onion key, if the document is signed by the operator it names and that operator is the one expected. */
|
|
100
|
+
function verifyOnionIdentity(doc, expectedOperator) {
|
|
101
|
+
try {
|
|
102
|
+
if (!doc || doc.operator !== expectedOperator)
|
|
103
|
+
return null;
|
|
104
|
+
const operator = new web3_js_1.PublicKey(doc.operator).toBytes();
|
|
105
|
+
const onionPub = hexBytes(doc.onion_pubkey, 32);
|
|
106
|
+
const signature = hexBytes(doc.signature, 64);
|
|
107
|
+
if (!onionPub || !signature || isZero(onionPub))
|
|
108
|
+
return null;
|
|
109
|
+
return tweetnacl_1.default.sign.detached.verify(onionIdentityMessage(onionPub, operator), signature, operator) ? onionPub : null;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Fetches each candidate's signed onion identity and keeps the ones that verify against the operator the caller took from the chain. */
|
|
116
|
+
async function loadOnionRelays(candidates, fetchFn = fetch, timeoutMs = 4000) {
|
|
117
|
+
const found = await Promise.all(candidates.map(async (c) => {
|
|
118
|
+
try {
|
|
119
|
+
const res = await fetchFn(`${c.endpoint.replace(/\/+$/, '')}/onion/identity`, { signal: AbortSignal.timeout(timeoutMs), redirect: 'error' });
|
|
120
|
+
if (!res.ok)
|
|
121
|
+
return null;
|
|
122
|
+
const key = verifyOnionIdentity(await res.json(), c.operator);
|
|
123
|
+
return key ? { ...c, onionKey: key } : null;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}));
|
|
129
|
+
return found.filter((r) => r !== null);
|
|
130
|
+
}
|
|
131
|
+
/** A random circuit of distinct relays run by distinct operators. */
|
|
132
|
+
function chooseCircuit(relays, hops = exports.ONION_MIN_HOPS, random = Math.random) {
|
|
133
|
+
if (hops < exports.ONION_MIN_HOPS || hops > exports.ONION_MAX_HOPS)
|
|
134
|
+
throw new Error(`A circuit has ${exports.ONION_MIN_HOPS} to ${exports.ONION_MAX_HOPS} relays`);
|
|
135
|
+
const pool = relays.filter((r, i) => relays.findIndex((o) => o.operator === r.operator) === i);
|
|
136
|
+
if (pool.length < hops)
|
|
137
|
+
throw new Error(`${pool.length} relays with verified keys are known and a circuit needs ${hops}`);
|
|
138
|
+
for (let i = pool.length - 1; i > 0; i--) {
|
|
139
|
+
const j = Math.floor(random() * (i + 1));
|
|
140
|
+
[pool[i], pool[j]] = [pool[j], pool[i]];
|
|
141
|
+
}
|
|
142
|
+
return pool.slice(0, hops);
|
|
143
|
+
}
|
|
144
|
+
/** Wraps the request in a circuit and submits it to the first relay. The last relay delivers it as a /stealth/send request. */
|
|
145
|
+
async function sendOnion(circuit, request, fetchFn = fetch) {
|
|
146
|
+
if (!hexBytes(request.recipient_view_pubkey, 32))
|
|
147
|
+
throw new Error('recipient_view_pubkey must be 32 bytes of hex');
|
|
148
|
+
const payload = new TextEncoder().encode(JSON.stringify(request));
|
|
149
|
+
const packet = await buildOnionPacket(circuit.map((r) => r.onionKey), payload);
|
|
150
|
+
const res = await fetchFn(`${circuit[0].endpoint.replace(/\/+$/, '')}/onion/relay`, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
153
|
+
body: packet,
|
|
154
|
+
redirect: 'error',
|
|
155
|
+
});
|
|
156
|
+
if (!res.ok)
|
|
157
|
+
throw new Error(`The first relay answered ${res.status}`);
|
|
158
|
+
return { firstRelay: circuit[0].operator, status: res.status };
|
|
159
|
+
}
|
package/dist/settlement.js
CHANGED
|
@@ -75,7 +75,7 @@ async function settleSession(env, args) {
|
|
|
75
75
|
session: args.session, vault, yieldVault: addr.yieldVault, rewardDistributor: addr.rewardDistributor, protocolConfig: addr.config,
|
|
76
76
|
zgapAuthority: addr.zgapAuthority, zgapMint: addr.zgapMint, zgaMint, zgapCollateralVault: addr.zgapCollateralVault,
|
|
77
77
|
zgapReserve: addr.zgapReserve, zgapReserveZgaAccount: (0, token_1.associatedTokenAddress)(zgaMint, addr.zgapReserve),
|
|
78
|
-
lpLockTokenAccount: addr.lpLock, guildTokenAccount: (0, token_1.associatedTokenAddress)(addr.zgapMint, guildAccount.
|
|
78
|
+
lpLockTokenAccount: addr.lpLock, guildTokenAccount: (0, token_1.associatedTokenAddress)(addr.zgapMint, guildAccount.authority),
|
|
79
79
|
gameManifest, gameCreatorAccount: addr.gameCreator(creator, gameId), royaltyVault: addr.royaltyVault(creator, gameId),
|
|
80
80
|
prizeVault: addr.prizeVault(args.session), guildAccount: session.guild,
|
|
81
81
|
tokenProgram: token_1.TOKEN_PROGRAM_ID, ixSysvar: web3_js_1.SYSVAR_INSTRUCTIONS_PUBKEY,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zga/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"files": ["dist"],
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
"test": "node --test test/*.test.mjs"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
+
"@hpke/chacha20poly1305": "1.8.0",
|
|
14
|
+
"@hpke/core": "1.9.0",
|
|
15
|
+
"@hpke/dhkem-x25519": "1.8.0",
|
|
13
16
|
"@zga/shielded-sdk": "*",
|
|
14
17
|
"tweetnacl": "^1.0.3"
|
|
15
18
|
},
|