@forgezero/runtime 0.1.0
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/LICENSE +21 -0
- package/README.md +89 -0
- package/contracts/foundry.toml +9 -0
- package/contracts/src/ColdVault.sol +206 -0
- package/contracts/src/DepositFactory.sol +202 -0
- package/contracts/src/DepositProxy.sol +72 -0
- package/contracts/src/IERC20.sol +7 -0
- package/contracts/src/MockTokens.sol +32 -0
- package/contracts/src/SafeTransferLib.sol +31 -0
- package/contracts/test/Custody.t.sol +361 -0
- package/contracts/test/Vectors.t.sol +45 -0
- package/dist/audit.d.ts +265 -0
- package/dist/audit.js +291 -0
- package/dist/backup.d.ts +243 -0
- package/dist/backup.js +302 -0
- package/dist/calendar.d.ts +136 -0
- package/dist/calendar.js +129 -0
- package/dist/compliance.d.ts +172 -0
- package/dist/compliance.js +168 -0
- package/dist/finance/binance.d.ts +27 -0
- package/dist/finance/binance.js +452 -0
- package/dist/finance/chain-addresses.d.ts +130 -0
- package/dist/finance/chain-addresses.js +462 -0
- package/dist/finance/chain-deposits.d.ts +193 -0
- package/dist/finance/chain-deposits.js +596 -0
- package/dist/finance/chain-reconcile.d.ts +112 -0
- package/dist/finance/chain-reconcile.js +76 -0
- package/dist/finance/chain-withdrawals.d.ts +223 -0
- package/dist/finance/chain-withdrawals.js +631 -0
- package/dist/finance/chain.d.ts +116 -0
- package/dist/finance/chain.js +316 -0
- package/dist/finance/commission.d.ts +155 -0
- package/dist/finance/commission.js +419 -0
- package/dist/finance/custody.d.ts +68 -0
- package/dist/finance/custody.js +107 -0
- package/dist/finance/derive.d.ts +115 -0
- package/dist/finance/derive.js +116 -0
- package/dist/finance/discounts.d.ts +98 -0
- package/dist/finance/discounts.js +90 -0
- package/dist/finance/ledger.d.ts +221 -0
- package/dist/finance/ledger.js +308 -0
- package/dist/finance/market.d.ts +209 -0
- package/dist/finance/market.js +112 -0
- package/dist/finance/money.d.ts +118 -0
- package/dist/finance/money.js +176 -0
- package/dist/finance/rates.d.ts +178 -0
- package/dist/finance/rates.js +292 -0
- package/dist/finance/storage.d.ts +113 -0
- package/dist/finance/storage.js +226 -0
- package/dist/finance/tax.d.ts +132 -0
- package/dist/finance/tax.js +291 -0
- package/dist/finance/transfers.d.ts +153 -0
- package/dist/finance/transfers.js +292 -0
- package/dist/finance/venues.d.ts +190 -0
- package/dist/finance/venues.js +251 -0
- package/dist/identity.d.ts +115 -0
- package/dist/identity.js +111 -0
- package/dist/importers.d.ts +87 -0
- package/dist/importers.js +250 -0
- package/dist/jobs.d.ts +171 -0
- package/dist/jobs.js +250 -0
- package/dist/notify-templates.d.ts +11 -0
- package/dist/notify-templates.js +254 -0
- package/dist/notify.d.ts +172 -0
- package/dist/notify.js +122 -0
- package/dist/openssh.d.ts +36 -0
- package/dist/openssh.js +106 -0
- package/dist/otpauth.d.ts +57 -0
- package/dist/otpauth.js +223 -0
- package/dist/outbox.d.ts +234 -0
- package/dist/outbox.js +236 -0
- package/dist/passkey.d.ts +120 -0
- package/dist/passkey.js +105 -0
- package/dist/phrase.d.ts +87 -0
- package/dist/phrase.js +87 -0
- package/dist/pipeline.d.ts +137 -0
- package/dist/pipeline.js +121 -0
- package/dist/queue.d.ts +243 -0
- package/dist/queue.js +246 -0
- package/dist/schema-typebox.d.ts +24 -0
- package/dist/schema-typebox.js +201 -0
- package/dist/schema.d.ts +134 -0
- package/dist/schema.js +169 -0
- package/dist/serial.d.ts +54 -0
- package/dist/serial.js +40 -0
- package/dist/slip10.d.ts +37 -0
- package/dist/slip10.js +74 -0
- package/dist/snp.d.ts +115 -0
- package/dist/snp.js +109 -0
- package/dist/ssh-agent.d.ts +70 -0
- package/dist/ssh-agent.js +141 -0
- package/dist/ssh-cert.d.ts +73 -0
- package/dist/ssh-cert.js +111 -0
- package/dist/totp.d.ts +104 -0
- package/dist/totp.js +143 -0
- package/package.json +248 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined")
|
|
5
|
+
return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/finance/money.ts
|
|
10
|
+
class MoneyError extends Error {
|
|
11
|
+
code;
|
|
12
|
+
constructor(code, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.name = "MoneyError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
var ASSETS = [
|
|
19
|
+
{ code: "USDT", decimals: 6 },
|
|
20
|
+
{ code: "USDC", decimals: 6 },
|
|
21
|
+
{ code: "BTC", decimals: 8 },
|
|
22
|
+
{ code: "ETH", decimals: 18 },
|
|
23
|
+
{ code: "BNB", decimals: 18 },
|
|
24
|
+
{ code: "EUR", decimals: 2 },
|
|
25
|
+
{ code: "USD", decimals: 2 }
|
|
26
|
+
];
|
|
27
|
+
var REGISTRY = new Map(ASSETS.map((asset) => [asset.code, asset]));
|
|
28
|
+
function defineAsset(spec) {
|
|
29
|
+
if (spec.decimals < 0 || spec.decimals > 30 || !Number.isInteger(spec.decimals)) {
|
|
30
|
+
throw new MoneyError("UNKNOWN_ASSET", `${spec.code}: decimals must be an integer 0–30.`);
|
|
31
|
+
}
|
|
32
|
+
REGISTRY.set(spec.code, spec);
|
|
33
|
+
}
|
|
34
|
+
function assetSpec(code) {
|
|
35
|
+
const spec = REGISTRY.get(code);
|
|
36
|
+
if (!spec)
|
|
37
|
+
throw new MoneyError("UNKNOWN_ASSET", `Unknown asset "${code}". Call defineAsset first.`);
|
|
38
|
+
return spec;
|
|
39
|
+
}
|
|
40
|
+
var money = (units, asset) => {
|
|
41
|
+
assetSpec(asset);
|
|
42
|
+
return { units, asset };
|
|
43
|
+
};
|
|
44
|
+
var zero = (asset) => money(0n, asset);
|
|
45
|
+
function parseAmount(value, asset) {
|
|
46
|
+
const spec = assetSpec(asset);
|
|
47
|
+
const text = value.trim();
|
|
48
|
+
if (!/^-?\d+(\.\d+)?$/.test(text)) {
|
|
49
|
+
throw new MoneyError("NOT_FINITE", `"${value}" is not a plain decimal amount.`);
|
|
50
|
+
}
|
|
51
|
+
const negative = text.startsWith("-");
|
|
52
|
+
const [whole, fraction = ""] = text.replace("-", "").split(".");
|
|
53
|
+
if (fraction.length > spec.decimals) {
|
|
54
|
+
throw new MoneyError("PRECISION_LOSS", `${asset} has ${spec.decimals} decimals; "${value}" has ${fraction.length}.`);
|
|
55
|
+
}
|
|
56
|
+
const padded = fraction.padEnd(spec.decimals, "0");
|
|
57
|
+
const units = BigInt(whole + padded);
|
|
58
|
+
return { units: negative ? -units : units, asset };
|
|
59
|
+
}
|
|
60
|
+
function formatAmount(amount, options = {}) {
|
|
61
|
+
const spec = assetSpec(amount.asset);
|
|
62
|
+
const negative = amount.units < 0n;
|
|
63
|
+
const digits = (negative ? -amount.units : amount.units).toString().padStart(spec.decimals + 1, "0");
|
|
64
|
+
const whole = digits.slice(0, digits.length - spec.decimals);
|
|
65
|
+
let fraction = spec.decimals === 0 ? "" : digits.slice(digits.length - spec.decimals);
|
|
66
|
+
if (options.trim && fraction)
|
|
67
|
+
fraction = fraction.replace(/0+$/, "");
|
|
68
|
+
return `${negative ? "-" : ""}${whole}${fraction ? `.${fraction}` : ""}`;
|
|
69
|
+
}
|
|
70
|
+
function sameAsset(a, b) {
|
|
71
|
+
if (a.asset !== b.asset) {
|
|
72
|
+
throw new MoneyError("ASSET_MISMATCH", `Cannot combine ${a.asset} and ${b.asset}.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function add(a, b) {
|
|
76
|
+
sameAsset(a, b);
|
|
77
|
+
return { units: a.units + b.units, asset: a.asset };
|
|
78
|
+
}
|
|
79
|
+
function subtract(a, b) {
|
|
80
|
+
sameAsset(a, b);
|
|
81
|
+
return { units: a.units - b.units, asset: a.asset };
|
|
82
|
+
}
|
|
83
|
+
var negate = (amount) => ({ units: -amount.units, asset: amount.asset });
|
|
84
|
+
var abs = (amount) => ({
|
|
85
|
+
units: amount.units < 0n ? -amount.units : amount.units,
|
|
86
|
+
asset: amount.asset
|
|
87
|
+
});
|
|
88
|
+
var isZero = (amount) => amount.units === 0n;
|
|
89
|
+
var isNegative = (amount) => amount.units < 0n;
|
|
90
|
+
function compare(a, b) {
|
|
91
|
+
sameAsset(a, b);
|
|
92
|
+
return a.units < b.units ? -1 : a.units > b.units ? 1 : 0;
|
|
93
|
+
}
|
|
94
|
+
var equals = (a, b) => a.asset === b.asset && a.units === b.units;
|
|
95
|
+
var ROUNDING = ["down", "up", "half-up"];
|
|
96
|
+
function divideRounded(numerator, denominator, mode) {
|
|
97
|
+
if (denominator === 0n)
|
|
98
|
+
throw new MoneyError("DIVIDE_BY_ZERO", "Division by zero.");
|
|
99
|
+
const negative = numerator < 0n !== denominator < 0n;
|
|
100
|
+
const a = numerator < 0n ? -numerator : numerator;
|
|
101
|
+
const b = denominator < 0n ? -denominator : denominator;
|
|
102
|
+
const quotient = a / b;
|
|
103
|
+
const remainder = a % b;
|
|
104
|
+
if (remainder === 0n)
|
|
105
|
+
return negative ? -quotient : quotient;
|
|
106
|
+
let result = quotient;
|
|
107
|
+
if (mode === "up")
|
|
108
|
+
result += 1n;
|
|
109
|
+
else if (mode === "half-up" && remainder * 2n >= b)
|
|
110
|
+
result += 1n;
|
|
111
|
+
return negative ? -result : result;
|
|
112
|
+
}
|
|
113
|
+
function mulRate(amount, rate, mode = "down") {
|
|
114
|
+
if (!/^-?\d+(\.\d+)?$/.test(rate.trim())) {
|
|
115
|
+
throw new MoneyError("NOT_FINITE", `"${rate}" is not a plain decimal rate.`);
|
|
116
|
+
}
|
|
117
|
+
const [whole, fraction = ""] = rate.trim().replace("-", "").split(".");
|
|
118
|
+
const scale = 10n ** BigInt(fraction.length);
|
|
119
|
+
const scaled = BigInt(whole + fraction) * (rate.trim().startsWith("-") ? -1n : 1n);
|
|
120
|
+
return { units: divideRounded(amount.units * scaled, scale, mode), asset: amount.asset };
|
|
121
|
+
}
|
|
122
|
+
function convert(amount, to, rate, mode = "down") {
|
|
123
|
+
const from = assetSpec(amount.asset);
|
|
124
|
+
const target = assetSpec(to);
|
|
125
|
+
const asTarget = mulRate({ units: amount.units, asset: to }, rate, mode);
|
|
126
|
+
const shift = target.decimals - from.decimals;
|
|
127
|
+
if (shift === 0)
|
|
128
|
+
return asTarget;
|
|
129
|
+
if (shift > 0)
|
|
130
|
+
return { units: asTarget.units * 10n ** BigInt(shift), asset: to };
|
|
131
|
+
return { units: divideRounded(asTarget.units, 10n ** BigInt(-shift), mode), asset: to };
|
|
132
|
+
}
|
|
133
|
+
function allocate(amount, parts) {
|
|
134
|
+
if (parts < 1)
|
|
135
|
+
throw new MoneyError("NOT_FINITE", "Cannot allocate into fewer than one part.");
|
|
136
|
+
const each = divideRounded(amount.units, BigInt(parts), "down");
|
|
137
|
+
const allocated = Array.from({ length: parts }, () => each);
|
|
138
|
+
let remainder = amount.units - each * BigInt(parts);
|
|
139
|
+
const step = remainder < 0n ? -1n : 1n;
|
|
140
|
+
for (let index = 0;remainder !== 0n; index = (index + 1) % parts) {
|
|
141
|
+
allocated[index] += step;
|
|
142
|
+
remainder -= step;
|
|
143
|
+
}
|
|
144
|
+
return allocated.map((units) => ({ units, asset: amount.asset }));
|
|
145
|
+
}
|
|
146
|
+
function toStep(amount, step, mode = "down") {
|
|
147
|
+
const stepUnits = parseAmount(step, amount.asset).units;
|
|
148
|
+
if (stepUnits <= 0n)
|
|
149
|
+
throw new MoneyError("NOT_FINITE", "A step must be positive.");
|
|
150
|
+
return { units: divideRounded(amount.units, stepUnits, mode) * stepUnits, asset: amount.asset };
|
|
151
|
+
}
|
|
152
|
+
var VERSION = "0.1.0";
|
|
153
|
+
|
|
154
|
+
// src/finance/venues.ts
|
|
155
|
+
var MARKET_TYPES = ["spot", "margin", "futures"];
|
|
156
|
+
var ORDER_SIDES = ["buy", "sell"];
|
|
157
|
+
var ORDER_TYPES = ["market", "limit", "stop-limit"];
|
|
158
|
+
var TIME_IN_FORCE = ["gtc", "ioc", "fok"];
|
|
159
|
+
var VENUES = [
|
|
160
|
+
{
|
|
161
|
+
key: "binance",
|
|
162
|
+
label: "Binance",
|
|
163
|
+
markets: ["spot", "margin", "futures"],
|
|
164
|
+
rateLimit: { perMinute: 6000, counts: "weight" },
|
|
165
|
+
clientOrderIds: true,
|
|
166
|
+
note: "Publishes no AAAA records, so every endpoint is IPv4-only and the budget is per IP."
|
|
167
|
+
}
|
|
168
|
+
];
|
|
169
|
+
var venue = (key) => VENUES.find((entry) => entry.key === key);
|
|
170
|
+
var venuesFor = (type) => VENUES.filter((entry) => entry.markets.includes(type));
|
|
171
|
+
var symbolOf = (base, quote) => `${base.toUpperCase()}/${quote.toUpperCase()}`;
|
|
172
|
+
function parseSymbol(symbol) {
|
|
173
|
+
const [base, quote] = symbol.toUpperCase().split("/");
|
|
174
|
+
if (!base || !quote)
|
|
175
|
+
throw new VenueError("BAD_SYMBOL", `"${symbol}" is not BASE/QUOTE.`);
|
|
176
|
+
return { base, quote };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
class VenueError extends Error {
|
|
180
|
+
code;
|
|
181
|
+
constructor(code, message) {
|
|
182
|
+
super(message);
|
|
183
|
+
this.code = code;
|
|
184
|
+
this.name = "VenueError";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function notionalOf(request, market) {
|
|
188
|
+
const price = request.price;
|
|
189
|
+
if (!price)
|
|
190
|
+
return parseAmount("0", market.quote);
|
|
191
|
+
const quantity = Number(formatAmount(request.quantity));
|
|
192
|
+
const priceText = formatAmount(price);
|
|
193
|
+
return parseAmount((quantity * Number(priceText)).toFixed(0) === "NaN" ? "0" : String(quantity * Number(priceText)), market.quote);
|
|
194
|
+
}
|
|
195
|
+
function validateOrder(request, market) {
|
|
196
|
+
const spec = venue(request.venue);
|
|
197
|
+
if (!spec)
|
|
198
|
+
throw new VenueError("UNKNOWN_VENUE", `Unknown venue "${request.venue}".`);
|
|
199
|
+
if (!spec.markets.includes(request.type)) {
|
|
200
|
+
throw new VenueError("MARKET_UNSUPPORTED", `${spec.label} does not offer ${request.type}.`);
|
|
201
|
+
}
|
|
202
|
+
if (market.venue !== request.venue || market.type !== request.type || market.symbol !== request.symbol) {
|
|
203
|
+
throw new VenueError("UNKNOWN_MARKET", "The market does not describe this order.");
|
|
204
|
+
}
|
|
205
|
+
if (request.orderType !== "market" && !request.price) {
|
|
206
|
+
throw new VenueError("PRICE_REQUIRED", `A ${request.orderType} order needs a price.`);
|
|
207
|
+
}
|
|
208
|
+
const snapped = toStep(request.quantity, market.lotStep);
|
|
209
|
+
if (compare(snapped, request.quantity) !== 0) {
|
|
210
|
+
throw new VenueError("LOT_STEP", `Quantity ${formatAmount(request.quantity, { trim: true })} is not a multiple of ${market.lotStep}.`);
|
|
211
|
+
}
|
|
212
|
+
if (request.price) {
|
|
213
|
+
const onTick = toStep(request.price, market.tickStep);
|
|
214
|
+
if (compare(onTick, request.price) !== 0) {
|
|
215
|
+
throw new VenueError("TICK_STEP", `Price ${formatAmount(request.price, { trim: true })} is not a multiple of ${market.tickStep}.`);
|
|
216
|
+
}
|
|
217
|
+
const notional = notionalOf(request, market);
|
|
218
|
+
if (compare(notional, parseAmount(market.minNotional, market.quote)) < 0) {
|
|
219
|
+
throw new VenueError("MIN_NOTIONAL", `Order is worth ${formatAmount(notional, { trim: true })} ${market.quote}; the minimum is ${market.minNotional}.`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (request.leverage !== undefined) {
|
|
223
|
+
if (request.type === "spot" || market.maxLeverage === undefined) {
|
|
224
|
+
throw new VenueError("LEVERAGE_UNSUPPORTED", `${request.type} markets have no leverage.`);
|
|
225
|
+
}
|
|
226
|
+
if (request.leverage < 1 || request.leverage > market.maxLeverage) {
|
|
227
|
+
throw new VenueError("LEVERAGE_TOO_HIGH", `Leverage ${request.leverage}× exceeds the ${market.maxLeverage}× maximum on this market.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function submitOrder(adapter, request, market) {
|
|
232
|
+
validateOrder(request, market);
|
|
233
|
+
return adapter.placeOrder(request, market);
|
|
234
|
+
}
|
|
235
|
+
var VERSION2 = "0.1.0";
|
|
236
|
+
export {
|
|
237
|
+
venuesFor,
|
|
238
|
+
venue,
|
|
239
|
+
validateOrder,
|
|
240
|
+
symbolOf,
|
|
241
|
+
submitOrder,
|
|
242
|
+
parseSymbol,
|
|
243
|
+
notionalOf,
|
|
244
|
+
VenueError,
|
|
245
|
+
VERSION2 as VERSION,
|
|
246
|
+
VENUES,
|
|
247
|
+
TIME_IN_FORCE,
|
|
248
|
+
ORDER_TYPES,
|
|
249
|
+
ORDER_SIDES,
|
|
250
|
+
MARKET_TYPES
|
|
251
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node identity — hybrid Ed25519 + ML-DSA-65 signatures.
|
|
3
|
+
*
|
|
4
|
+
* A node key is long-lived and its signatures are recorded, which is exactly the
|
|
5
|
+
* harvest-now-verify-later shape: an adversary who breaks Ed25519 in ten years
|
|
6
|
+
* can forge today's traffic retroactively. ML-DSA-65 (FIPS 204) closes that.
|
|
7
|
+
*
|
|
8
|
+
* HYBRID, not pure post-quantum. Both signatures must verify. Lattice schemes
|
|
9
|
+
* are young; Ed25519 is not. Requiring both means a break in either one leaves
|
|
10
|
+
* the other standing, and the cost is a few hundred bytes per request.
|
|
11
|
+
*
|
|
12
|
+
* This is the one place ForgeZero uses asymmetric post-quantum crypto. Custody
|
|
13
|
+
* needs none — it is symmetric and Shamir throughout — but a node signature is
|
|
14
|
+
* genuinely a signature, so there is a Shor target here and nowhere else.
|
|
15
|
+
*
|
|
16
|
+
* ## Where the primitives come from
|
|
17
|
+
*
|
|
18
|
+
* Two sources, and `node:crypto` is not one of them. It was, and it made this
|
|
19
|
+
* the single module in the package set that could not run in a browser, an edge
|
|
20
|
+
* worker or a Cloudflare Worker — for SHA-256 and HKDF, which Web Crypto and
|
|
21
|
+
* noble both provide.
|
|
22
|
+
*
|
|
23
|
+
* Web Crypto randomness, and the base64url helpers in access/security
|
|
24
|
+
* noble SHA-256, HKDF, Ed25519, ML-DSA-65
|
|
25
|
+
*
|
|
26
|
+
* Noble rather than `crypto.subtle` for the hashes because everything here is
|
|
27
|
+
* synchronous and `subtle.digest` is not; making `canonicalString` async to
|
|
28
|
+
* avoid one dependency the module already carries would be a worse trade.
|
|
29
|
+
*/
|
|
30
|
+
export interface NodeKeyPair {
|
|
31
|
+
ed25519: {
|
|
32
|
+
publicKey: string;
|
|
33
|
+
secretKey: string;
|
|
34
|
+
};
|
|
35
|
+
mlDsa: {
|
|
36
|
+
publicKey: string;
|
|
37
|
+
secretKey: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export interface NodePublicKeys {
|
|
41
|
+
ed25519: string;
|
|
42
|
+
mlDsa: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Derive a key pair DETERMINISTICALLY from a seed.
|
|
46
|
+
*
|
|
47
|
+
* This is what makes an API key work as a credential without our database
|
|
48
|
+
* holding anything authenticable: the key handed to a customer IS the seed, the
|
|
49
|
+
* client re-derives the same pair on every start, and we store only the public
|
|
50
|
+
* halves. A dump of `api_keys` yields nothing that can sign.
|
|
51
|
+
*
|
|
52
|
+
* The two algorithms get SEPARATE subkeys via HKDF rather than sharing the seed
|
|
53
|
+
* bytes. Feeding identical material to an Ed25519 secret and an ML-DSA keygen
|
|
54
|
+
* would tie the two schemes together — the hybrid is only worth its bytes if a
|
|
55
|
+
* break in one leaves the other genuinely independent.
|
|
56
|
+
*/
|
|
57
|
+
export declare function deriveKeysFromSeed(seed: Uint8Array): NodeKeyPair;
|
|
58
|
+
/** Generate a node key pair. The secret halves never leave the caller. */
|
|
59
|
+
export declare function generateNodeKeys(): NodeKeyPair;
|
|
60
|
+
export interface SignedEnvelope {
|
|
61
|
+
nodeKey: string;
|
|
62
|
+
timestamp: number;
|
|
63
|
+
nonce: string;
|
|
64
|
+
edSignature: string;
|
|
65
|
+
mlDsaSignature: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The bytes that get signed.
|
|
69
|
+
*
|
|
70
|
+
* Method and path are included so a valid signature cannot be replayed against
|
|
71
|
+
* a different endpoint. The BODY DIGEST is signed rather than the body itself,
|
|
72
|
+
* so a proxy may re-frame the request but may not alter its content — and so a
|
|
73
|
+
* large upload does not have to be buffered twice.
|
|
74
|
+
*/
|
|
75
|
+
export declare function canonicalString(args: {
|
|
76
|
+
method: string;
|
|
77
|
+
path: string;
|
|
78
|
+
query: string;
|
|
79
|
+
timestamp: number;
|
|
80
|
+
nonce: string;
|
|
81
|
+
body: string | Uint8Array;
|
|
82
|
+
}): string;
|
|
83
|
+
export declare function signRequest(keys: NodeKeyPair, nodeKey: string, args: {
|
|
84
|
+
method: string;
|
|
85
|
+
path: string;
|
|
86
|
+
query?: string;
|
|
87
|
+
body: string | Uint8Array;
|
|
88
|
+
}): SignedEnvelope;
|
|
89
|
+
export type VerifyFailure = 'timestamp_out_of_window' | 'ed25519_invalid' | 'ml_dsa_invalid' | 'malformed';
|
|
90
|
+
/** Requests older or newer than this are refused before any signature work. */
|
|
91
|
+
export declare const CLOCK_SKEW_SECONDS = 300;
|
|
92
|
+
/**
|
|
93
|
+
* Verify both signatures over the canonical string.
|
|
94
|
+
*
|
|
95
|
+
* The timestamp window is checked FIRST and cheaply. Two reasons: it bounds how
|
|
96
|
+
* long the replay-nonce table has to remember anything, and it means an attacker
|
|
97
|
+
* cannot make the server do lattice verification work for free.
|
|
98
|
+
*
|
|
99
|
+
* BOTH signatures must pass. Accepting either alone would silently reduce the
|
|
100
|
+
* scheme to whichever is weaker.
|
|
101
|
+
*/
|
|
102
|
+
export declare function verifyRequest(args: {
|
|
103
|
+
envelope: SignedEnvelope;
|
|
104
|
+
publicKeys: NodePublicKeys;
|
|
105
|
+
method: string;
|
|
106
|
+
path: string;
|
|
107
|
+
query?: string;
|
|
108
|
+
body: string | Uint8Array;
|
|
109
|
+
nowSeconds?: number;
|
|
110
|
+
}): {
|
|
111
|
+
verified: true;
|
|
112
|
+
} | {
|
|
113
|
+
verified: false;
|
|
114
|
+
reason: VerifyFailure;
|
|
115
|
+
};
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined")
|
|
5
|
+
return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/identity.ts
|
|
10
|
+
import { toBase64Url, fromBase64Url } from "@forgezero/access/security";
|
|
11
|
+
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
12
|
+
import { ml_dsa65 } from "@noble/post-quantum/ml-dsa.js";
|
|
13
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
14
|
+
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
15
|
+
var ENCODER = new TextEncoder;
|
|
16
|
+
var b64 = toBase64Url;
|
|
17
|
+
var un64 = fromBase64Url;
|
|
18
|
+
var randomBytes = (length) => crypto.getRandomValues(new Uint8Array(length));
|
|
19
|
+
function deriveKeysFromSeed(seed) {
|
|
20
|
+
if (seed.length < 32)
|
|
21
|
+
throw new Error("identity: seed must be at least 32 bytes");
|
|
22
|
+
const sub = (label) => hkdf(sha256, seed, undefined, ENCODER.encode(label), 32);
|
|
23
|
+
const edSecret = sub("forgezero/identity/ed25519/v1");
|
|
24
|
+
const mlSeed = sub("forgezero/identity/ml-dsa-65/v1");
|
|
25
|
+
const edPublic = ed25519.getPublicKey(edSecret);
|
|
26
|
+
const mlKeys = ml_dsa65.keygen(mlSeed);
|
|
27
|
+
mlSeed.fill(0);
|
|
28
|
+
return {
|
|
29
|
+
ed25519: { publicKey: b64(edPublic), secretKey: b64(edSecret) },
|
|
30
|
+
mlDsa: { publicKey: b64(mlKeys.publicKey), secretKey: b64(mlKeys.secretKey) }
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function generateNodeKeys() {
|
|
34
|
+
const edSecret = ed25519.utils.randomSecretKey();
|
|
35
|
+
const edPublic = ed25519.getPublicKey(edSecret);
|
|
36
|
+
const seed = randomBytes(32);
|
|
37
|
+
const mlKeys = ml_dsa65.keygen(seed);
|
|
38
|
+
seed.fill(0);
|
|
39
|
+
return {
|
|
40
|
+
ed25519: { publicKey: b64(edPublic), secretKey: b64(edSecret) },
|
|
41
|
+
mlDsa: { publicKey: b64(mlKeys.publicKey), secretKey: b64(mlKeys.secretKey) }
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function canonicalString(args) {
|
|
45
|
+
const body = typeof args.body === "string" ? ENCODER.encode(args.body) : args.body;
|
|
46
|
+
const digest = Array.from(sha256(body), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
47
|
+
return [
|
|
48
|
+
args.method.toUpperCase(),
|
|
49
|
+
args.path,
|
|
50
|
+
args.query ?? "",
|
|
51
|
+
String(args.timestamp),
|
|
52
|
+
args.nonce,
|
|
53
|
+
digest
|
|
54
|
+
].join(`
|
|
55
|
+
`);
|
|
56
|
+
}
|
|
57
|
+
function signRequest(keys, nodeKey, args) {
|
|
58
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
59
|
+
const nonce = b64(randomBytes(16));
|
|
60
|
+
const message = ENCODER.encode(canonicalString({ ...args, query: args.query ?? "", timestamp, nonce }));
|
|
61
|
+
return {
|
|
62
|
+
nodeKey,
|
|
63
|
+
timestamp,
|
|
64
|
+
nonce,
|
|
65
|
+
edSignature: b64(ed25519.sign(message, un64(keys.ed25519.secretKey))),
|
|
66
|
+
mlDsaSignature: b64(ml_dsa65.sign(message, un64(keys.mlDsa.secretKey)))
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
var CLOCK_SKEW_SECONDS = 300;
|
|
70
|
+
function verifyRequest(args) {
|
|
71
|
+
const now = args.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
72
|
+
if (Math.abs(now - args.envelope.timestamp) > CLOCK_SKEW_SECONDS) {
|
|
73
|
+
return { verified: false, reason: "timestamp_out_of_window" };
|
|
74
|
+
}
|
|
75
|
+
let message;
|
|
76
|
+
try {
|
|
77
|
+
message = ENCODER.encode(canonicalString({
|
|
78
|
+
method: args.method,
|
|
79
|
+
path: args.path,
|
|
80
|
+
query: args.query ?? "",
|
|
81
|
+
timestamp: args.envelope.timestamp,
|
|
82
|
+
nonce: args.envelope.nonce,
|
|
83
|
+
body: args.body
|
|
84
|
+
}));
|
|
85
|
+
} catch {
|
|
86
|
+
return { verified: false, reason: "malformed" };
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
if (!ed25519.verify(un64(args.envelope.edSignature), message, un64(args.publicKeys.ed25519))) {
|
|
90
|
+
return { verified: false, reason: "ed25519_invalid" };
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
return { verified: false, reason: "ed25519_invalid" };
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
if (!ml_dsa65.verify(un64(args.envelope.mlDsaSignature), message, un64(args.publicKeys.mlDsa))) {
|
|
97
|
+
return { verified: false, reason: "ml_dsa_invalid" };
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
return { verified: false, reason: "ml_dsa_invalid" };
|
|
101
|
+
}
|
|
102
|
+
return { verified: true };
|
|
103
|
+
}
|
|
104
|
+
export {
|
|
105
|
+
verifyRequest,
|
|
106
|
+
signRequest,
|
|
107
|
+
generateNodeKeys,
|
|
108
|
+
deriveKeysFromSeed,
|
|
109
|
+
canonicalString,
|
|
110
|
+
CLOCK_SKEW_SECONDS
|
|
111
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading secrets out of the tools people are leaving.
|
|
3
|
+
*
|
|
4
|
+
* Adoption is the whole reason this exists. Somebody with three hundred
|
|
5
|
+
* credentials in 1Password will not retype them, and an import that mangles
|
|
6
|
+
* twenty of those three hundred is worse than none — they will not find out
|
|
7
|
+
* which twenty until something fails in production.
|
|
8
|
+
*
|
|
9
|
+
* So every parser here is built around the same three rules:
|
|
10
|
+
*
|
|
11
|
+
* NOTHING IS GUESSED. A row that cannot be understood is reported as skipped
|
|
12
|
+
* with a reason, never imported under a name the parser invented.
|
|
13
|
+
*
|
|
14
|
+
* NO VALUE IS EVER IN AN ERROR. Every message names the LINE or the ITEM, and
|
|
15
|
+
* the one thing a parse error must not do is put a password in a log.
|
|
16
|
+
*
|
|
17
|
+
* NAMES ARE NORMALISED THE SAME WAY EVERY TIME, so re-running an import
|
|
18
|
+
* updates what it created rather than producing a second copy under a
|
|
19
|
+
* slightly different name.
|
|
20
|
+
*/
|
|
21
|
+
export interface ImportedSecret {
|
|
22
|
+
/** Normalised. `DATABASE_URL`, not `Database URL`. */
|
|
23
|
+
name: string;
|
|
24
|
+
value: string;
|
|
25
|
+
/** Where it came from, for a review screen. Never the value. */
|
|
26
|
+
source: string;
|
|
27
|
+
note?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface Skipped {
|
|
30
|
+
/** A line number, an item title — something a human can find. */
|
|
31
|
+
at: string;
|
|
32
|
+
reason: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ImportResult {
|
|
35
|
+
secrets: readonly ImportedSecret[];
|
|
36
|
+
skipped: readonly Skipped[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `Database URL` and `database-url` become `DATABASE_URL`.
|
|
40
|
+
*
|
|
41
|
+
* One rule, applied everywhere, because the alternative is an import that
|
|
42
|
+
* creates `DATABASE_URL` on Monday and `DATABASE__URL` on Tuesday and leaves a
|
|
43
|
+
* developer with two entries that look identical in a list.
|
|
44
|
+
*/
|
|
45
|
+
export declare function normaliseName(raw: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* A `.env` file.
|
|
48
|
+
*
|
|
49
|
+
* Handles what real files contain: `export` prefixes, quoted values,
|
|
50
|
+
* `#` comments, blank lines, and values containing `=`. It does NOT expand
|
|
51
|
+
* `${OTHER}` references — an expansion done here would bake today's value of
|
|
52
|
+
* another variable into a stored secret, and the point of a vault is that the
|
|
53
|
+
* value is resolved when it is read.
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseEnv(text: string): ImportResult;
|
|
56
|
+
/**
|
|
57
|
+
* A CSV export.
|
|
58
|
+
*
|
|
59
|
+
* The header decides which column holds what, and a file whose header names no
|
|
60
|
+
* recognisable value column is REFUSED rather than guessed at — importing
|
|
61
|
+
* column two because it looked passwordy is how a set of usernames ends up
|
|
62
|
+
* stored as secrets.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseCsv(text: string): ImportResult;
|
|
65
|
+
/**
|
|
66
|
+
* A Bitwarden JSON export.
|
|
67
|
+
*
|
|
68
|
+
* Folders and cards are ignored deliberately: a card number is not a secret an
|
|
69
|
+
* application reads at runtime, and importing one puts a payment instrument in
|
|
70
|
+
* a place designed for machine credentials. Logins and secure notes are what
|
|
71
|
+
* belong here.
|
|
72
|
+
*
|
|
73
|
+
* An ENCRYPTED export is refused with the reason, because the alternative is a
|
|
74
|
+
* parse that "succeeds" and imports a list of ciphertext blobs.
|
|
75
|
+
*/
|
|
76
|
+
export declare function parseBitwarden(text: string): ImportResult;
|
|
77
|
+
/**
|
|
78
|
+
* A 1Password `.1pux` item list, or its CSV export.
|
|
79
|
+
*
|
|
80
|
+
* The CSV path reuses `parseCsv` — 1Password's CSV header already uses `title`
|
|
81
|
+
* and `password`, which that parser recognises. This handles the JSON form,
|
|
82
|
+
* where the value lives in a field list rather than a fixed column.
|
|
83
|
+
*/
|
|
84
|
+
export declare function parseOnePassword(text: string): ImportResult;
|
|
85
|
+
export declare const IMPORT_FORMATS: readonly ["env", "csv", "bitwarden", "1password"];
|
|
86
|
+
export type ImportFormat = (typeof IMPORT_FORMATS)[number];
|
|
87
|
+
export declare function parseImport(format: ImportFormat, text: string): ImportResult;
|