@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
package/dist/schema.js ADDED
@@ -0,0 +1,169 @@
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/schema.ts
10
+ class SchemaError extends Error {
11
+ code;
12
+ path;
13
+ constructor(code, message, path) {
14
+ super(message);
15
+ this.code = code;
16
+ this.path = path;
17
+ this.name = "SchemaError";
18
+ }
19
+ }
20
+ var DEFAULT_RESTRICTIONS = {
21
+ maxDepth: 4,
22
+ maxFields: 100,
23
+ maxBytes: 64 * 1024,
24
+ forbidden: ["$ref", "$id", "$dynamicRef", "$dynamicAnchor", "$schema", "definitions", "$defs"]
25
+ };
26
+ function restrictJsonSchema(schema, limits = DEFAULT_RESTRICTIONS) {
27
+ const bytes = JSON.stringify(schema).length;
28
+ if (bytes > limits.maxBytes) {
29
+ throw new SchemaError("SCHEMA_TOO_LARGE", `Schema is ${bytes} bytes; the limit is ${limits.maxBytes}.`);
30
+ }
31
+ let fields = 0;
32
+ const walk = (node, depth, path) => {
33
+ if (node === null || typeof node !== "object")
34
+ return;
35
+ if (Array.isArray(node)) {
36
+ node.forEach((child, index) => walk(child, depth, `${path}[${index}]`));
37
+ return;
38
+ }
39
+ const record = node;
40
+ for (const keyword of limits.forbidden) {
41
+ if (keyword in record) {
42
+ throw new SchemaError("SCHEMA_FORBIDDEN_KEYWORD", `"${keyword}" is not permitted: it can point validation at a document we do not control.`, path || "(root)");
43
+ }
44
+ }
45
+ if (depth > limits.maxDepth) {
46
+ throw new SchemaError("SCHEMA_TOO_DEEP", `Nesting exceeds ${limits.maxDepth} levels.`, path || "(root)");
47
+ }
48
+ if (record.type === "object") {
49
+ const properties = record.properties ?? {};
50
+ const names = Object.keys(properties);
51
+ fields += names.length;
52
+ if (fields > limits.maxFields) {
53
+ throw new SchemaError("SCHEMA_TOO_WIDE", `Schema declares more than ${limits.maxFields} fields.`, path || "(root)");
54
+ }
55
+ if (record.additionalProperties !== false) {
56
+ throw new SchemaError("SCHEMA_OPEN_OBJECT", "Objects must set additionalProperties:false, or unvalidated fields pass through.", path || "(root)");
57
+ }
58
+ for (const name of names) {
59
+ walk(properties[name], depth + 1, path ? `${path}.${name}` : name);
60
+ }
61
+ }
62
+ if (record.type === "array" && record.items) {
63
+ walk(record.items, depth + 1, `${path}[]`);
64
+ }
65
+ };
66
+ walk(schema, 1, "");
67
+ return schema;
68
+ }
69
+ function deriveAnnotation(property) {
70
+ const raw = property["x-fz-derive"];
71
+ if (typeof raw !== "object" || raw === null)
72
+ return;
73
+ const spec = raw;
74
+ const scheme = typeof spec.scheme === "string" ? spec.scheme : undefined;
75
+ if (!scheme)
76
+ return;
77
+ return {
78
+ custody: spec.custody === "supplied" ? "supplied" : "derived",
79
+ scheme,
80
+ expose: typeof spec.expose === "string" ? spec.expose : undefined
81
+ };
82
+ }
83
+ function describeJsonSchema(schema, prefix = "") {
84
+ if (schema.type !== "object")
85
+ return [];
86
+ const properties = schema.properties ?? {};
87
+ const required = new Set(schema.required ?? []);
88
+ return Object.entries(properties).map(([name, property]) => {
89
+ const path = prefix ? `${prefix}.${name}` : name;
90
+ const kind = fieldKind(property);
91
+ const field = {
92
+ path,
93
+ label: property.title ?? humanise(name),
94
+ kind,
95
+ required: required.has(name),
96
+ description: property.description,
97
+ writeOnly: property.writeOnly === true ? true : undefined,
98
+ default: property.default,
99
+ format: property.format,
100
+ minLength: property.minLength,
101
+ maxLength: property.maxLength,
102
+ minimum: property.minimum,
103
+ maximum: property.maximum,
104
+ pattern: property.pattern,
105
+ derive: deriveAnnotation(property)
106
+ };
107
+ if (kind === "enum") {
108
+ return { ...field, options: property.enum ?? property.anyOf };
109
+ }
110
+ if (kind === "object") {
111
+ return { ...field, items: describeJsonSchema(property, path) };
112
+ }
113
+ if (kind === "array" && property.items) {
114
+ return {
115
+ ...field,
116
+ items: describeJsonSchema(property.items, path)
117
+ };
118
+ }
119
+ return field;
120
+ });
121
+ }
122
+ function fieldKind(property) {
123
+ if (Array.isArray(property.enum))
124
+ return "enum";
125
+ if (Array.isArray(property.anyOf) && property.anyOf.every((m) => typeof m === "object" && m !== null && ("const" in m))) {
126
+ return "enum";
127
+ }
128
+ switch (property.type) {
129
+ case "string":
130
+ return "string";
131
+ case "number":
132
+ return "number";
133
+ case "integer":
134
+ return "integer";
135
+ case "boolean":
136
+ return "boolean";
137
+ case "array":
138
+ return "array";
139
+ case "object":
140
+ return "object";
141
+ default:
142
+ return "unknown";
143
+ }
144
+ }
145
+ function humanise(name) {
146
+ const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim().toLowerCase();
147
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
148
+ }
149
+ function readableFields(fields) {
150
+ return fields.filter((field) => !field.writeOnly).map((field) => field.items ? { ...field, items: readableFields(field.items) } : field);
151
+ }
152
+ function writeOnlyPaths(fields) {
153
+ return fields.flatMap((field) => [
154
+ ...field.writeOnly ? [field.path] : [],
155
+ ...field.items ? writeOnlyPaths(field.items) : []
156
+ ]);
157
+ }
158
+ var SCHEMA_VERSION = 1;
159
+ var VERSION = "0.1.0";
160
+ export {
161
+ writeOnlyPaths,
162
+ restrictJsonSchema,
163
+ readableFields,
164
+ describeJsonSchema,
165
+ VERSION,
166
+ SchemaError,
167
+ SCHEMA_VERSION,
168
+ DEFAULT_RESTRICTIONS
169
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Run work for one key strictly in order, and give the caller the result back.
3
+ *
4
+ * ## Why this is not `@forgezero/runtime/queue`
5
+ *
6
+ * The queue is for work that happens LATER: it persists a message, hands it to
7
+ * a handler, retries, dead-letters. It is the right answer when the caller does
8
+ * not need the outcome — a deposit credit, a webhook, a scan.
9
+ *
10
+ * This is for work that happens NOW and whose result the caller returns. A
11
+ * request writing a ledger posting has to answer with the posting; enqueuing it
12
+ * would mean replying "accepted" to something the caller needs to have
13
+ * happened. Two different problems, and using the queue for this one would mean
14
+ * inventing a way to wait for a message — which is a worse version of this file.
15
+ *
16
+ * It was written twice before it was written once: the audit chain needed it to
17
+ * keep sequence numbers gapless, and accounts needed it to stop two credits
18
+ * interleaving. Two copies of a concurrency primitive is two chances to get it
19
+ * wrong, so it lives here.
20
+ *
21
+ * ## What it does NOT give you
22
+ *
23
+ * Ordering within ONE process. Two API nodes each hold their own chain, so a
24
+ * durable guarantee still needs a unique index on whatever must not happen
25
+ * twice. This makes the common case cheap and correct; the index makes every
26
+ * case correct. Anything relying on this alone across a cluster is relying on
27
+ * there being one node, which stops being true without warning.
28
+ */
29
+ export interface SerialOptions {
30
+ /**
31
+ * Keys to keep chains for.
32
+ *
33
+ * A chain per key is a promise per key, and a process serving a million
34
+ * accounts would hold a million of them forever. Idle chains are dropped once
35
+ * settled, so the map holds only keys with work in flight.
36
+ */
37
+ maxKeys?: number;
38
+ }
39
+ export declare function createKeyedSerial(options?: SerialOptions): {
40
+ /**
41
+ * Run `work` after everything already queued for this key.
42
+ *
43
+ * A rejection does NOT poison the chain: the tail is always replaced with a
44
+ * settled promise, so one transient failure cannot take every subsequent
45
+ * call for that key with it — which is how a single database blip becomes a
46
+ * permanently stuck account.
47
+ */
48
+ run<T>(key: string, work: () => Promise<T>): Promise<T>;
49
+ /** How many keys have work in flight. For a health endpoint. */
50
+ size: () => number;
51
+ /** Wait for everything currently queued. Tests, and a graceful shutdown. */
52
+ drain: () => Promise<void>;
53
+ };
54
+ export type KeyedSerial = ReturnType<typeof createKeyedSerial>;
package/dist/serial.js ADDED
@@ -0,0 +1,40 @@
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/serial.ts
10
+ function createKeyedSerial(options = {}) {
11
+ const maxKeys = options.maxKeys ?? 1e4;
12
+ const chains = new Map;
13
+ return {
14
+ run(key, work) {
15
+ const previous = chains.get(key) ?? Promise.resolve();
16
+ const next = previous.then(work, work);
17
+ const settled = next.then(() => {
18
+ return;
19
+ }, () => {
20
+ return;
21
+ });
22
+ chains.set(key, settled);
23
+ settled.then(() => {
24
+ if (chains.get(key) === settled)
25
+ chains.delete(key);
26
+ });
27
+ if (chains.size > maxKeys) {
28
+ console.warn(`[serial] ${chains.size} keys in flight, above the ${maxKeys} guideline.`);
29
+ }
30
+ return next;
31
+ },
32
+ size: () => chains.size,
33
+ drain: async () => {
34
+ await Promise.allSettled([...chains.values()]);
35
+ }
36
+ };
37
+ }
38
+ export {
39
+ createKeyedSerial
40
+ };
@@ -0,0 +1,37 @@
1
+ /** SLIP-0010 hardens by adding 2^31. Anything below it is a public index. */
2
+ export declare const HARDENED_OFFSET = 2147483648;
3
+ export declare class Slip10Error extends Error {
4
+ readonly code: 'NOT_HARDENED' | 'BAD_PATH' | 'BAD_SEED';
5
+ constructor(code: 'NOT_HARDENED' | 'BAD_PATH' | 'BAD_SEED', message: string);
6
+ }
7
+ export interface Slip10Node {
8
+ /** 32 bytes. The ed25519 secret scalar seed, not an expanded key. */
9
+ key: Uint8Array;
10
+ chainCode: Uint8Array;
11
+ }
12
+ /**
13
+ * The master node.
14
+ *
15
+ * Seeds shorter than 16 bytes are refused. SLIP-0010 permits 128 to 512 bits
16
+ * and a caller passing something smaller has almost certainly passed a
17
+ * passphrase rather than a seed — accepting it would derive a whole key tree
18
+ * from guessable entropy.
19
+ */
20
+ export declare function masterFromSeed(seed: Uint8Array): Slip10Node;
21
+ /**
22
+ * One hardened step.
23
+ *
24
+ * The data is `0x00 || key || index`, and the leading zero byte is not padding
25
+ * — it is what distinguishes this from BIP-32's secp256k1 construction, which
26
+ * puts a compressed public key there for a public step. Omitting it produces
27
+ * plausible keys that match nothing.
28
+ */
29
+ export declare function deriveChild(node: Slip10Node, index: number): Slip10Node;
30
+ /**
31
+ * `m/44'/501'/0'/0'` — every component hardened.
32
+ *
33
+ * Accepts `'` or `h` as the hardening mark, because both appear in the wild and
34
+ * a path copied from a wallet that writes `h` should not silently derive a
35
+ * different key than the same path written with an apostrophe.
36
+ */
37
+ export declare function derivePath(seed: Uint8Array, path: string): Slip10Node;
package/dist/slip10.js ADDED
@@ -0,0 +1,74 @@
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/slip10.ts
10
+ import { hmac } from "@noble/hashes/hmac.js";
11
+ import { sha512 } from "@noble/hashes/sha2.js";
12
+ var ED25519_SEED = new TextEncoder().encode("ed25519 seed");
13
+ var HARDENED_OFFSET = 2147483648;
14
+
15
+ class Slip10Error extends Error {
16
+ code;
17
+ constructor(code, message) {
18
+ super(message);
19
+ this.code = code;
20
+ this.name = "Slip10Error";
21
+ }
22
+ }
23
+ function masterFromSeed(seed) {
24
+ if (seed.length < 16 || seed.length > 64) {
25
+ throw new Slip10Error("BAD_SEED", "A SLIP-0010 seed is 16 to 64 bytes.");
26
+ }
27
+ const digest = hmac(sha512, ED25519_SEED, seed);
28
+ return { key: digest.slice(0, 32), chainCode: digest.slice(32) };
29
+ }
30
+ function deriveChild(node, index) {
31
+ if (!Number.isInteger(index) || index < 0 || index > 4294967295) {
32
+ throw new Slip10Error("BAD_PATH", `${index} is not a derivation index.`);
33
+ }
34
+ if (index < HARDENED_OFFSET) {
35
+ throw new Slip10Error("NOT_HARDENED", `ed25519 has no public derivation. Index ${index} must be hardened — SLIP-0010 defines only hardened steps for this curve.`);
36
+ }
37
+ const data = new Uint8Array(1 + 32 + 4);
38
+ data[0] = 0;
39
+ data.set(node.key, 1);
40
+ new DataView(data.buffer).setUint32(33, index >>> 0, false);
41
+ const digest = hmac(sha512, node.chainCode, data);
42
+ data.fill(0);
43
+ return { key: digest.slice(0, 32), chainCode: digest.slice(32) };
44
+ }
45
+ function derivePath(seed, path) {
46
+ const trimmed = path.trim();
47
+ if (!/^m(\/\d+['h]?)*$/.test(trimmed)) {
48
+ throw new Slip10Error("BAD_PATH", `"${path}" is not a derivation path.`);
49
+ }
50
+ let node = masterFromSeed(seed);
51
+ const parts = trimmed.split("/").slice(1).filter(Boolean);
52
+ for (const part of parts) {
53
+ const hardened = part.endsWith("'") || part.endsWith("h");
54
+ const raw = Number.parseInt(hardened ? part.slice(0, -1) : part, 10);
55
+ if (!hardened) {
56
+ throw new Slip10Error("NOT_HARDENED", `"${path}" contains a non-hardened component (${part}). ed25519 derivation is hardened-only.`);
57
+ }
58
+ if (raw >= HARDENED_OFFSET) {
59
+ throw new Slip10Error("BAD_PATH", `Index ${raw} is already past the hardened offset.`);
60
+ }
61
+ const next = deriveChild(node, raw + HARDENED_OFFSET);
62
+ node.key.fill(0);
63
+ node.chainCode.fill(0);
64
+ node = next;
65
+ }
66
+ return node;
67
+ }
68
+ export {
69
+ masterFromSeed,
70
+ derivePath,
71
+ deriveChild,
72
+ Slip10Error,
73
+ HARDENED_OFFSET
74
+ };
package/dist/snp.d.ts ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Parsing an AMD SEV-SNP attestation report.
3
+ *
4
+ * The report is a fixed 1184-byte structure the PSP produces and signs. Every
5
+ * offset below comes from the SEV Secure Nested Paging Firmware ABI
6
+ * specification, table "ATTESTATION_REPORT Structure" — they are not derived
7
+ * from anything and must not be adjusted to make a sample parse.
8
+ *
9
+ * ## Why this is separate from verifying it
10
+ *
11
+ * Parsing is total and testable: given bytes, produce fields or refuse. Deciding
12
+ * whether those fields are ACCEPTABLE is policy, and it already lives in
13
+ * `api/src/server/compute/attestation.ts`. Keeping them apart means a parser bug
14
+ * cannot be mistaken for a policy decision, and a policy change does not require
15
+ * touching byte offsets.
16
+ *
17
+ * ## What this does NOT do
18
+ *
19
+ * It does not verify the signature. Chaining a report to AMD's root needs the
20
+ * VCEK certificate for that specific chip at that specific TCB, fetched from
21
+ * AMD's KDS — a network dependency with its own failure modes, and a
22
+ * hand-rolled ECDSA-P384 verification here would be worse than none because it
23
+ * would produce `verified: true` for a report nobody signed.
24
+ *
25
+ * So `parseSnpReport` returns the signature bytes and says nothing about them.
26
+ * The caller supplies a verifier, which is the same shape every other trust
27
+ * decision in this codebase takes.
28
+ */
29
+ /** The structure is exactly this long. Anything else is not a report. */
30
+ export declare const REPORT_BYTES = 1184;
31
+ export declare class SnpError extends Error {
32
+ readonly code: 'BAD_LENGTH' | 'BAD_VERSION' | 'BAD_VMPL';
33
+ constructor(code: 'BAD_LENGTH' | 'BAD_VERSION' | 'BAD_VMPL', message: string);
34
+ }
35
+ /**
36
+ * Guest policy bits.
37
+ *
38
+ * `debug` is the one that matters most and the one an operator is most likely
39
+ * to leave on: a guest launched with debugging enabled can have its memory
40
+ * inspected by the hypervisor, which removes the entire property SEV-SNP is
41
+ * bought for. It is surfaced by name rather than as a bitmask so a policy check
42
+ * reads as a sentence.
43
+ */
44
+ export interface GuestPolicy {
45
+ /** Minimum ABI the guest requires. */
46
+ abiMinor: number;
47
+ abiMajor: number;
48
+ /** SMT permitted on the host. */
49
+ smt: boolean;
50
+ /** Migration agent permitted — the guest's memory may leave this machine. */
51
+ migrateMa: boolean;
52
+ /** Debugging enabled. Memory is inspectable; this must be false in production. */
53
+ debug: boolean;
54
+ /** The guest must run on a single socket. */
55
+ singleSocket: boolean;
56
+ /** The raw value, so a caller can record exactly what was reported. */
57
+ raw: string;
58
+ }
59
+ export interface SnpReport {
60
+ version: number;
61
+ guestSvn: number;
62
+ policy: GuestPolicy;
63
+ vmpl: number;
64
+ /** 48 bytes of hex. What the guest actually booted. */
65
+ measurement: string;
66
+ /** 64 bytes of hex. Whatever the guest asked the PSP to bind in — our nonce. */
67
+ reportData: string;
68
+ hostData: string;
69
+ /** Identifies the physical chip. Two reports from one machine share it. */
70
+ chipId: string;
71
+ /** TCB the report was produced under. Lower than a floor is a downgrade. */
72
+ reportedTcb: TcbVersion;
73
+ currentTcb: TcbVersion;
74
+ /** 512 bytes. Returned, never interpreted here. */
75
+ signature: string;
76
+ }
77
+ /** The four component versions packed into a TCB_VERSION. */
78
+ export interface TcbVersion {
79
+ bootLoader: number;
80
+ tee: number;
81
+ snp: number;
82
+ microcode: number;
83
+ raw: string;
84
+ }
85
+ /**
86
+ * Bytes to fields.
87
+ *
88
+ * Refuses before reading anything when the length is wrong. A short buffer read
89
+ * with a DataView produces either an exception at an arbitrary offset or, worse,
90
+ * plausible values from adjacent memory — and a measurement assembled from
91
+ * whatever followed the buffer is the one failure that must never look like a
92
+ * successful parse.
93
+ */
94
+ export declare function parseSnpReport(bytes: Uint8Array): SnpReport;
95
+ /**
96
+ * Is every TCB component at least the floor?
97
+ *
98
+ * Field by field, and ALL of them. A single packed comparison would let a
99
+ * microcode bump mask an SNP firmware downgrade — which is precisely the
100
+ * direction an attacker wants, because older firmware is where the known
101
+ * vulnerabilities are.
102
+ */
103
+ export declare function tcbAtLeast(reported: TcbVersion, floor: TcbVersion): boolean;
104
+ /**
105
+ * Does the report bind OUR challenge?
106
+ *
107
+ * `REPORT_DATA` is 64 bytes the guest chooses and the PSP signs. A nonce placed
108
+ * there is what makes a report fresh rather than replayed — without this check a
109
+ * report captured once is valid for ever, and attestation degrades into "this
110
+ * machine was an SNP guest at some point".
111
+ *
112
+ * Compared over the whole field, including the zero padding, so a nonce shorter
113
+ * than 64 bytes cannot be matched by a report that merely starts with it.
114
+ */
115
+ export declare function bindsNonce(report: SnpReport, nonce: string): boolean;
package/dist/snp.js ADDED
@@ -0,0 +1,109 @@
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/snp.ts
10
+ var REPORT_BYTES = 1184;
11
+ var OFFSET = {
12
+ version: 0,
13
+ guestSvn: 4,
14
+ policy: 8,
15
+ familyId: 16,
16
+ imageId: 32,
17
+ vmpl: 48,
18
+ signatureAlgo: 52,
19
+ currentTcb: 56,
20
+ platformInfo: 64,
21
+ reportData: 80,
22
+ measurement: 144,
23
+ hostData: 192,
24
+ idKeyDigest: 224,
25
+ authorKeyDigest: 272,
26
+ reportId: 320,
27
+ reportIdMa: 352,
28
+ reportedTcb: 384,
29
+ chipId: 416,
30
+ signature: 672
31
+ };
32
+
33
+ class SnpError extends Error {
34
+ code;
35
+ constructor(code, message) {
36
+ super(message);
37
+ this.code = code;
38
+ this.name = "SnpError";
39
+ }
40
+ }
41
+ var hex = (bytes) => [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
42
+ function readTcb(view, offset) {
43
+ const raw = new Uint8Array(view.buffer, view.byteOffset + offset, 8);
44
+ return {
45
+ bootLoader: raw[0],
46
+ tee: raw[1],
47
+ snp: raw[6],
48
+ microcode: raw[7],
49
+ raw: hex(raw)
50
+ };
51
+ }
52
+ function readPolicy(view, offset) {
53
+ const low = view.getUint32(offset, true);
54
+ const high = view.getUint32(offset + 4, true);
55
+ const bit = (index) => (low >>> index & 1) === 1;
56
+ return {
57
+ abiMinor: low & 255,
58
+ abiMajor: low >>> 8 & 255,
59
+ smt: bit(16),
60
+ migrateMa: bit(18),
61
+ debug: bit(19),
62
+ singleSocket: bit(20),
63
+ raw: `${high.toString(16).padStart(8, "0")}${low.toString(16).padStart(8, "0")}`
64
+ };
65
+ }
66
+ function parseSnpReport(bytes) {
67
+ if (bytes.length !== REPORT_BYTES) {
68
+ throw new SnpError("BAD_LENGTH", `An SEV-SNP attestation report is ${REPORT_BYTES} bytes; this is ${bytes.length}.`);
69
+ }
70
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
71
+ const version = view.getUint32(OFFSET.version, true);
72
+ if (version !== 2 && version !== 3) {
73
+ throw new SnpError("BAD_VERSION", `Report version ${version} is not one this parser understands.`);
74
+ }
75
+ const vmpl = view.getUint32(OFFSET.vmpl, true);
76
+ if (vmpl > 3) {
77
+ throw new SnpError("BAD_VMPL", `VMPL ${vmpl} is outside the defined range.`);
78
+ }
79
+ const slice = (offset, length) => hex(bytes.subarray(offset, offset + length));
80
+ return {
81
+ version,
82
+ guestSvn: view.getUint32(OFFSET.guestSvn, true),
83
+ policy: readPolicy(view, OFFSET.policy),
84
+ vmpl,
85
+ measurement: slice(OFFSET.measurement, 48),
86
+ reportData: slice(OFFSET.reportData, 64),
87
+ hostData: slice(OFFSET.hostData, 32),
88
+ chipId: slice(OFFSET.chipId, 64),
89
+ reportedTcb: readTcb(view, OFFSET.reportedTcb),
90
+ currentTcb: readTcb(view, OFFSET.currentTcb),
91
+ signature: slice(OFFSET.signature, 512)
92
+ };
93
+ }
94
+ function tcbAtLeast(reported, floor) {
95
+ return reported.bootLoader >= floor.bootLoader && reported.tee >= floor.tee && reported.snp >= floor.snp && reported.microcode >= floor.microcode;
96
+ }
97
+ function bindsNonce(report, nonce) {
98
+ const bare = nonce.replace(/^0x/, "").toLowerCase();
99
+ if (bare.length === 0 || bare.length > 128 || bare.length % 2 !== 0)
100
+ return false;
101
+ return report.reportData === bare.padEnd(128, "0");
102
+ }
103
+ export {
104
+ tcbAtLeast,
105
+ parseSnpReport,
106
+ bindsNonce,
107
+ SnpError,
108
+ REPORT_BYTES
109
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * SSH agent as a custody factor.
3
+ *
4
+ * The ceremony seals every custodian share twice — once under a key derived from
5
+ * a WebAuthn PRF output, once under a key derived from a BIP-39 phrase — and
6
+ * either alone opens it. That works in a browser and not at all over SSH, which
7
+ * is where the platform is first brought up: there is no passkey before there is
8
+ * a platform to register one against.
9
+ *
10
+ * An SSH agent fills the same slot. Both are an agent-held key that produces a
11
+ * stable secret without ever exposing the key itself, so the SSH signature over
12
+ * a fixed challenge substitutes for the PRF output with no change to the sealing
13
+ * code. The custodian later registers a passkey through the UI and the same
14
+ * share gains a browser route.
15
+ *
16
+ * **Ed25519 only, and that is a correctness requirement rather than a
17
+ * preference.** The derived key must be identical on every enrolment and every
18
+ * unlock, so the signature has to be deterministic. Ed25519 is (RFC 8032). RSA
19
+ * with PKCS#1 v1.5 happens to be, but agents are free to offer RSA-PSS for the
20
+ * same key, and PSS is randomised — a share sealed under one PSS signature could
21
+ * never be opened again. Refusing everything except Ed25519 makes that
22
+ * impossible rather than rare.
23
+ */
24
+ export declare class SshAgentError extends Error {
25
+ }
26
+ export interface AgentIdentity {
27
+ /** Raw SSH public key blob, as the agent returns it. */
28
+ blob: Buffer;
29
+ comment: string;
30
+ /** `ssh-ed25519`, etc. Anything else is refused — see the module note. */
31
+ type: string;
32
+ /** `SHA256:…`, matching `ssh-add -l`, so a human can confirm which key. */
33
+ fingerprint: string;
34
+ }
35
+ /**
36
+ * How long to wait on the agent before giving up.
37
+ *
38
+ * An agent holding a forwarded key whose upstream is gone, or a confirm-on-use
39
+ * key with nobody at the terminal, does not refuse — it simply never answers.
40
+ * Without a socket-level deadline those connections are never closed, and a
41
+ * caller that races them against its own timer leaks one socket per attempt
42
+ * until the agent stops accepting connections entirely.
43
+ */
44
+ export declare const AGENT_TIMEOUT_MS = 3000;
45
+ /** The keys the agent is holding. Matches `ssh-add -l`. */
46
+ export declare function listIdentities(socketPath?: string): Promise<AgentIdentity[]>;
47
+ /** Ed25519 only. See the module note — determinism is the whole mechanism. */
48
+ export declare function listCustodyIdentities(socketPath?: string): Promise<AgentIdentity[]>;
49
+ /**
50
+ * Derive the 32-byte custody key for an identity.
51
+ *
52
+ * Stable across processes and machines for the same key, which is what lets a
53
+ * custodian enrol today and unlock next month from a different laptop with the
54
+ * same key in their agent.
55
+ *
56
+ * HKDF over the signature rather than the signature itself: the signature is a
57
+ * value the agent will hand to anything that asks, so using it directly as a
58
+ * key would mean any process that can reach the socket holds the custody key.
59
+ * The salt and info bind it to this purpose.
60
+ */
61
+ export declare function deriveCustodyKey(identity: AgentIdentity, socketPath?: string): Promise<Uint8Array>;
62
+ /**
63
+ * Prove the derivation reproduces before it is trusted with a share.
64
+ *
65
+ * Signs twice and compares. A non-deterministic agent — a smartcard doing PSS, a
66
+ * forwarded agent that swapped keys mid-ceremony — would otherwise seal a share
67
+ * under a key that can never be reproduced, and the failure would surface only
68
+ * at the worst possible moment: recovery.
69
+ */
70
+ export declare function assertDeterministic(identity: AgentIdentity, socketPath?: string): Promise<Uint8Array>;