@forgezero/runtime 0.1.4 → 0.1.6
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/README.md +24 -2
- package/dist/backup.d.ts +19 -4
- package/dist/backup.js +252 -45
- package/dist/custody-crypto.d.ts +7 -5
- package/dist/custody-crypto.js +43 -21
- package/dist/custody-share.d.ts +5 -0
- package/dist/custody-share.js +46 -21
- package/dist/finance/chain-deposits.js +5 -1
- package/dist/finance/chain-withdrawals.js +5 -1
- package/dist/finance/commission.js +5 -1
- package/dist/finance/ledger.d.ts +8 -2
- package/dist/finance/ledger.js +6 -1
- package/dist/identity.d.ts +11 -3
- package/dist/identity.js +109 -14
- package/dist/jobs.d.ts +2 -0
- package/dist/jobs.js +9 -2
- package/dist/passkey-hybrid.d.ts +37 -0
- package/dist/passkey-hybrid.js +111 -0
- package/dist/query.d.ts +47 -0
- package/dist/query.js +51 -0
- package/dist/realtime.d.ts +68 -0
- package/dist/realtime.js +184 -0
- package/dist/schema-typebox.d.ts +3 -0
- package/dist/schema-typebox.js +32 -2
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +15 -2
- package/package.json +18 -10
- package/dist/ssh-agent.d.ts +0 -83
- package/dist/ssh-agent.js +0 -147
package/dist/ssh-agent.d.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
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
|
-
* Sign arbitrary bytes with an identity the agent holds.
|
|
51
|
-
*
|
|
52
|
-
* Exported because a fresh proof from a terminal needs it: the server issues a
|
|
53
|
-
* nonce and this is what turns it into something the server can verify against
|
|
54
|
-
* a registered public key. `deriveCustodyKey` signs a FIXED challenge and hashes
|
|
55
|
-
* the result — that is a key-derivation, not a proof, and using it as one would
|
|
56
|
-
* replay.
|
|
57
|
-
*
|
|
58
|
-
* Returns the raw ed25519 signature, unwrapped from the agent's blob, because
|
|
59
|
-
* that is what a verifier takes.
|
|
60
|
-
*/
|
|
61
|
-
export declare function signWithIdentity(identity: AgentIdentity, data: Uint8Array, socketPath?: string): Promise<Uint8Array>;
|
|
62
|
-
/**
|
|
63
|
-
* Derive the 32-byte custody key for an identity.
|
|
64
|
-
*
|
|
65
|
-
* Stable across processes and machines for the same key, which is what lets a
|
|
66
|
-
* custodian enrol today and unlock next month from a different laptop with the
|
|
67
|
-
* same key in their agent.
|
|
68
|
-
*
|
|
69
|
-
* HKDF over the signature rather than the signature itself: the signature is a
|
|
70
|
-
* value the agent will hand to anything that asks, so using it directly as a
|
|
71
|
-
* key would mean any process that can reach the socket holds the custody key.
|
|
72
|
-
* The salt and info bind it to this purpose.
|
|
73
|
-
*/
|
|
74
|
-
export declare function deriveCustodyKey(identity: AgentIdentity, socketPath?: string): Promise<Uint8Array>;
|
|
75
|
-
/**
|
|
76
|
-
* Prove the derivation reproduces before it is trusted with a share.
|
|
77
|
-
*
|
|
78
|
-
* Signs twice and compares. A non-deterministic agent — a smartcard doing PSS, a
|
|
79
|
-
* forwarded agent that swapped keys mid-ceremony — would otherwise seal a share
|
|
80
|
-
* under a key that can never be reproduced, and the failure would surface only
|
|
81
|
-
* at the worst possible moment: recovery.
|
|
82
|
-
*/
|
|
83
|
-
export declare function assertDeterministic(identity: AgentIdentity, socketPath?: string): Promise<Uint8Array>;
|
package/dist/ssh-agent.js
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
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/ssh-agent.ts
|
|
10
|
-
import { Socket } from "node:net";
|
|
11
|
-
import { createHash, hkdfSync } from "node:crypto";
|
|
12
|
-
|
|
13
|
-
class SshAgentError extends Error {
|
|
14
|
-
}
|
|
15
|
-
var SSH_AGENTC_REQUEST_IDENTITIES = 11;
|
|
16
|
-
var SSH_AGENT_IDENTITIES_ANSWER = 12;
|
|
17
|
-
var SSH_AGENTC_SIGN_REQUEST = 13;
|
|
18
|
-
var SSH_AGENT_SIGN_RESPONSE = 14;
|
|
19
|
-
var CUSTODY_CHALLENGE = Buffer.from("forgezero/custody/ssh-agent/v1", "utf8");
|
|
20
|
-
function readString(buffer, offset) {
|
|
21
|
-
const length = buffer.readUInt32BE(offset);
|
|
22
|
-
const start = offset + 4;
|
|
23
|
-
return [buffer.subarray(start, start + length), start + length];
|
|
24
|
-
}
|
|
25
|
-
function writeString(value) {
|
|
26
|
-
const length = Buffer.alloc(4);
|
|
27
|
-
length.writeUInt32BE(value.length);
|
|
28
|
-
return Buffer.concat([length, value]);
|
|
29
|
-
}
|
|
30
|
-
function frame(payload) {
|
|
31
|
-
const length = Buffer.alloc(4);
|
|
32
|
-
length.writeUInt32BE(payload.length);
|
|
33
|
-
return Buffer.concat([length, payload]);
|
|
34
|
-
}
|
|
35
|
-
var AGENT_TIMEOUT_MS = 3000;
|
|
36
|
-
async function request(socketPath, payload) {
|
|
37
|
-
return new Promise((resolve, reject) => {
|
|
38
|
-
const socket = new Socket;
|
|
39
|
-
const chunks = [];
|
|
40
|
-
let expected = null;
|
|
41
|
-
let settled = false;
|
|
42
|
-
const fail = (message) => {
|
|
43
|
-
if (settled)
|
|
44
|
-
return;
|
|
45
|
-
settled = true;
|
|
46
|
-
socket.destroy();
|
|
47
|
-
reject(new SshAgentError(message));
|
|
48
|
-
};
|
|
49
|
-
socket.setTimeout(AGENT_TIMEOUT_MS, () => fail("SSH_AGENT_TIMEOUT"));
|
|
50
|
-
socket.on("error", () => fail("SSH_AGENT_UNREACHABLE"));
|
|
51
|
-
socket.on("connect", () => socket.write(frame(payload)));
|
|
52
|
-
socket.on("data", (chunk) => {
|
|
53
|
-
chunks.push(chunk);
|
|
54
|
-
const all = Buffer.concat(chunks);
|
|
55
|
-
if (expected === null && all.length >= 4)
|
|
56
|
-
expected = all.readUInt32BE(0);
|
|
57
|
-
if (expected !== null && all.length >= expected + 4) {
|
|
58
|
-
if (settled)
|
|
59
|
-
return;
|
|
60
|
-
settled = true;
|
|
61
|
-
socket.end();
|
|
62
|
-
resolve(all.subarray(4, expected + 4));
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
socket.on("close", () => {
|
|
66
|
-
if (expected === null)
|
|
67
|
-
fail("SSH_AGENT_CLOSED_EARLY");
|
|
68
|
-
});
|
|
69
|
-
socket.connect(socketPath);
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
function agentSocket(explicit) {
|
|
73
|
-
const path = explicit ?? process.env.SSH_AUTH_SOCK;
|
|
74
|
-
if (!path)
|
|
75
|
-
throw new SshAgentError("SSH_AUTH_SOCK_NOT_SET");
|
|
76
|
-
return path;
|
|
77
|
-
}
|
|
78
|
-
async function listIdentities(socketPath) {
|
|
79
|
-
const response = await request(agentSocket(socketPath), Buffer.from([SSH_AGENTC_REQUEST_IDENTITIES]));
|
|
80
|
-
if (response[0] !== SSH_AGENT_IDENTITIES_ANSWER) {
|
|
81
|
-
throw new SshAgentError("SSH_AGENT_BAD_RESPONSE");
|
|
82
|
-
}
|
|
83
|
-
const count = response.readUInt32BE(1);
|
|
84
|
-
const identities = [];
|
|
85
|
-
let offset = 5;
|
|
86
|
-
for (let index = 0;index < count; index += 1) {
|
|
87
|
-
const [blob, afterBlob] = readString(response, offset);
|
|
88
|
-
const [comment, afterComment] = readString(response, afterBlob);
|
|
89
|
-
offset = afterComment;
|
|
90
|
-
const [type] = readString(blob, 0);
|
|
91
|
-
identities.push({
|
|
92
|
-
blob,
|
|
93
|
-
comment: comment.toString("utf8"),
|
|
94
|
-
type: type.toString("utf8"),
|
|
95
|
-
fingerprint: `SHA256:${createHash("sha256").update(blob).digest("base64").replace(/=+$/, "")}`
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
return identities;
|
|
99
|
-
}
|
|
100
|
-
async function listCustodyIdentities(socketPath) {
|
|
101
|
-
return (await listIdentities(socketPath)).filter((id) => id.type === "ssh-ed25519");
|
|
102
|
-
}
|
|
103
|
-
async function signWithIdentity(identity, data, socketPath) {
|
|
104
|
-
const wrapped = await sign(identity.blob, Buffer.from(data), socketPath);
|
|
105
|
-
const [raw] = readString(wrapped, readString(wrapped, 0)[1]);
|
|
106
|
-
return new Uint8Array(raw);
|
|
107
|
-
}
|
|
108
|
-
async function sign(blob, data, socketPath) {
|
|
109
|
-
const payload = Buffer.concat([
|
|
110
|
-
Buffer.from([SSH_AGENTC_SIGN_REQUEST]),
|
|
111
|
-
writeString(blob),
|
|
112
|
-
writeString(data),
|
|
113
|
-
Buffer.alloc(4)
|
|
114
|
-
]);
|
|
115
|
-
const response = await request(agentSocket(socketPath), payload);
|
|
116
|
-
if (response[0] !== SSH_AGENT_SIGN_RESPONSE) {
|
|
117
|
-
throw new SshAgentError("SSH_AGENT_SIGN_REFUSED");
|
|
118
|
-
}
|
|
119
|
-
const [signature] = readString(response, 1);
|
|
120
|
-
return signature;
|
|
121
|
-
}
|
|
122
|
-
async function deriveCustodyKey(identity, socketPath) {
|
|
123
|
-
if (identity.type !== "ssh-ed25519") {
|
|
124
|
-
throw new SshAgentError("SSH_KEY_TYPE_UNSUPPORTED");
|
|
125
|
-
}
|
|
126
|
-
const signature = await sign(identity.blob, CUSTODY_CHALLENGE, socketPath);
|
|
127
|
-
if (signature.length < 32)
|
|
128
|
-
throw new SshAgentError("SSH_AGENT_SIGNATURE_TOO_SHORT");
|
|
129
|
-
return new Uint8Array(hkdfSync("sha256", signature, identity.blob, Buffer.from("forgezero/custody/ssh-key/v1", "utf8"), 32));
|
|
130
|
-
}
|
|
131
|
-
async function assertDeterministic(identity, socketPath) {
|
|
132
|
-
const first = await deriveCustodyKey(identity, socketPath);
|
|
133
|
-
const second = await deriveCustodyKey(identity, socketPath);
|
|
134
|
-
if (Buffer.compare(Buffer.from(first), Buffer.from(second)) !== 0) {
|
|
135
|
-
throw new SshAgentError("SSH_AGENT_NOT_DETERMINISTIC");
|
|
136
|
-
}
|
|
137
|
-
return first;
|
|
138
|
-
}
|
|
139
|
-
export {
|
|
140
|
-
signWithIdentity,
|
|
141
|
-
listIdentities,
|
|
142
|
-
listCustodyIdentities,
|
|
143
|
-
deriveCustodyKey,
|
|
144
|
-
assertDeterministic,
|
|
145
|
-
SshAgentError,
|
|
146
|
-
AGENT_TIMEOUT_MS
|
|
147
|
-
};
|