@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.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +89 -0
  3. package/contracts/foundry.toml +9 -0
  4. package/contracts/src/ColdVault.sol +206 -0
  5. package/contracts/src/DepositFactory.sol +202 -0
  6. package/contracts/src/DepositProxy.sol +72 -0
  7. package/contracts/src/IERC20.sol +7 -0
  8. package/contracts/src/MockTokens.sol +32 -0
  9. package/contracts/src/SafeTransferLib.sol +31 -0
  10. package/contracts/test/Custody.t.sol +361 -0
  11. package/contracts/test/Vectors.t.sol +45 -0
  12. package/dist/audit.d.ts +265 -0
  13. package/dist/audit.js +291 -0
  14. package/dist/backup.d.ts +243 -0
  15. package/dist/backup.js +302 -0
  16. package/dist/calendar.d.ts +136 -0
  17. package/dist/calendar.js +129 -0
  18. package/dist/compliance.d.ts +172 -0
  19. package/dist/compliance.js +168 -0
  20. package/dist/finance/binance.d.ts +27 -0
  21. package/dist/finance/binance.js +452 -0
  22. package/dist/finance/chain-addresses.d.ts +130 -0
  23. package/dist/finance/chain-addresses.js +462 -0
  24. package/dist/finance/chain-deposits.d.ts +193 -0
  25. package/dist/finance/chain-deposits.js +596 -0
  26. package/dist/finance/chain-reconcile.d.ts +112 -0
  27. package/dist/finance/chain-reconcile.js +76 -0
  28. package/dist/finance/chain-withdrawals.d.ts +223 -0
  29. package/dist/finance/chain-withdrawals.js +631 -0
  30. package/dist/finance/chain.d.ts +116 -0
  31. package/dist/finance/chain.js +316 -0
  32. package/dist/finance/commission.d.ts +155 -0
  33. package/dist/finance/commission.js +419 -0
  34. package/dist/finance/custody.d.ts +68 -0
  35. package/dist/finance/custody.js +107 -0
  36. package/dist/finance/derive.d.ts +115 -0
  37. package/dist/finance/derive.js +116 -0
  38. package/dist/finance/discounts.d.ts +98 -0
  39. package/dist/finance/discounts.js +90 -0
  40. package/dist/finance/ledger.d.ts +221 -0
  41. package/dist/finance/ledger.js +308 -0
  42. package/dist/finance/market.d.ts +209 -0
  43. package/dist/finance/market.js +112 -0
  44. package/dist/finance/money.d.ts +118 -0
  45. package/dist/finance/money.js +176 -0
  46. package/dist/finance/rates.d.ts +178 -0
  47. package/dist/finance/rates.js +292 -0
  48. package/dist/finance/storage.d.ts +113 -0
  49. package/dist/finance/storage.js +226 -0
  50. package/dist/finance/tax.d.ts +132 -0
  51. package/dist/finance/tax.js +291 -0
  52. package/dist/finance/transfers.d.ts +153 -0
  53. package/dist/finance/transfers.js +292 -0
  54. package/dist/finance/venues.d.ts +190 -0
  55. package/dist/finance/venues.js +251 -0
  56. package/dist/identity.d.ts +115 -0
  57. package/dist/identity.js +111 -0
  58. package/dist/importers.d.ts +87 -0
  59. package/dist/importers.js +250 -0
  60. package/dist/jobs.d.ts +171 -0
  61. package/dist/jobs.js +250 -0
  62. package/dist/notify-templates.d.ts +11 -0
  63. package/dist/notify-templates.js +254 -0
  64. package/dist/notify.d.ts +172 -0
  65. package/dist/notify.js +122 -0
  66. package/dist/openssh.d.ts +36 -0
  67. package/dist/openssh.js +106 -0
  68. package/dist/otpauth.d.ts +57 -0
  69. package/dist/otpauth.js +223 -0
  70. package/dist/outbox.d.ts +234 -0
  71. package/dist/outbox.js +236 -0
  72. package/dist/passkey.d.ts +120 -0
  73. package/dist/passkey.js +105 -0
  74. package/dist/phrase.d.ts +87 -0
  75. package/dist/phrase.js +87 -0
  76. package/dist/pipeline.d.ts +137 -0
  77. package/dist/pipeline.js +121 -0
  78. package/dist/queue.d.ts +243 -0
  79. package/dist/queue.js +246 -0
  80. package/dist/schema-typebox.d.ts +24 -0
  81. package/dist/schema-typebox.js +201 -0
  82. package/dist/schema.d.ts +134 -0
  83. package/dist/schema.js +169 -0
  84. package/dist/serial.d.ts +54 -0
  85. package/dist/serial.js +40 -0
  86. package/dist/slip10.d.ts +37 -0
  87. package/dist/slip10.js +74 -0
  88. package/dist/snp.d.ts +115 -0
  89. package/dist/snp.js +109 -0
  90. package/dist/ssh-agent.d.ts +70 -0
  91. package/dist/ssh-agent.js +141 -0
  92. package/dist/ssh-cert.d.ts +73 -0
  93. package/dist/ssh-cert.js +111 -0
  94. package/dist/totp.d.ts +104 -0
  95. package/dist/totp.js +143 -0
  96. 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
+ };
@@ -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;