@drakon-systems/ekho-sdk 0.6.2 → 0.7.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/CHANGELOG.md +5 -0
- package/README.md +4 -0
- package/dist/esm/identity/index.d.ts +5 -0
- package/dist/esm/identity/index.d.ts.map +1 -0
- package/dist/esm/identity/index.js +11 -0
- package/dist/esm/identity/index.js.map +1 -0
- package/dist/esm/identity/primitives.d.ts +53 -0
- package/dist/esm/identity/primitives.d.ts.map +1 -0
- package/dist/esm/identity/primitives.js +116 -0
- package/dist/esm/identity/primitives.js.map +1 -0
- package/dist/esm/identity/store.d.ts +67 -0
- package/dist/esm/identity/store.d.ts.map +1 -0
- package/dist/esm/identity/store.js +116 -0
- package/dist/esm/identity/store.js.map +1 -0
- package/dist/esm/identity/verification.d.ts +152 -0
- package/dist/esm/identity/verification.d.ts.map +1 -0
- package/dist/esm/identity/verification.js +492 -0
- package/dist/esm/identity/verification.js.map +1 -0
- package/dist/esm/identity/verify.d.ts +41 -0
- package/dist/esm/identity/verify.d.ts.map +1 -0
- package/dist/esm/identity/verify.js +92 -0
- package/dist/esm/identity/verify.js.map +1 -0
- package/dist/identity/index.d.ts +5 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +27 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/identity/primitives.d.ts +53 -0
- package/dist/identity/primitives.d.ts.map +1 -0
- package/dist/identity/primitives.js +133 -0
- package/dist/identity/primitives.js.map +1 -0
- package/dist/identity/store.d.ts +67 -0
- package/dist/identity/store.d.ts.map +1 -0
- package/dist/identity/store.js +127 -0
- package/dist/identity/store.js.map +1 -0
- package/dist/identity/verification.d.ts +152 -0
- package/dist/identity/verification.d.ts.map +1 -0
- package/dist/identity/verification.js +503 -0
- package/dist/identity/verification.js.map +1 -0
- package/dist/identity/verify.d.ts +41 -0
- package/dist/identity/verify.d.ts.map +1 -0
- package/dist/identity/verify.js +96 -0
- package/dist/identity/verify.js.map +1 -0
- package/package.json +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@ All notable changes to Ekho are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.7.0] - 2026-10-12
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`@drakon-systems/ekho-sdk/identity`**: the verifiable-identity layer (Ed25519 primitives with the frozen canonical form, the identity file with atomic save and refuse-to-mint rules, inbound verification, operator-key pin sync, outbound v2 signing), moved verbatim from the OpenClaw plugin so the plugin and the new MCP connector run the identical code. Published for both `import` (`dist/esm/identity`) and `require` (`dist/identity`). The root export is unchanged; nothing existing is removed or renamed, so this is not a breaking change.
|
|
11
|
+
|
|
7
12
|
## [0.6.2] - 2026-10-08
|
|
8
13
|
|
|
9
14
|
### Changed
|
package/README.md
CHANGED
|
@@ -6,6 +6,10 @@ Agent SDK for [Ekho](https://github.com/Drakon-Systems-Ltd/ekho) — the private
|
|
|
6
6
|
|
|
7
7
|
What changed in this version: [CHANGELOG.md](./CHANGELOG.md). The changelog ships inside the published package, so a consumer can read it after `npm install` without visiting GitHub.
|
|
8
8
|
|
|
9
|
+
## Identity layer
|
|
10
|
+
|
|
11
|
+
`@drakon-systems/ekho-sdk/identity` is the verifiable-identity layer every TypeScript Ekho agent shares: Ed25519 primitives with the frozen canonical form, the identity file (`loadOrCreateIdentity`, `saveIdentity`: atomic owner-only writes, no re-minting unless told to), inbound verification (`verifyInbound`, `verifyBatch`), operator-key pin sync with the first-contact latch and signed revocation (`syncPinnedOperatorKeys`) and outbound v2 signing (`buildSignedSendFields`). The OpenClaw plugin and the MCP connector both run this code. It has no dependencies beyond `node:crypto`.
|
|
12
|
+
|
|
9
13
|
## Compatibility
|
|
10
14
|
|
|
11
15
|
- **0.4.1 — breaking (#12).** Post to a room with `recipient: {kind: "group", id: <room id>}`. Any other recipient kind under a room `conversation_id` is now a 400.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/identity/index.ts"],"names":[],"mappings":"AAMA,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// `@drakon-systems/ekho-sdk/identity` — the verifiable-identity layer every
|
|
2
|
+
// TypeScript Ekho agent shares: Ed25519 primitives with the frozen canonical
|
|
3
|
+
// form, the identity file (atomic save, refuse-to-mint rules), inbound
|
|
4
|
+
// verification, operator-key pin sync and outbound signing. Extracted from the
|
|
5
|
+
// OpenClaw plugin so the MCP connector signs and verifies with the identical
|
|
6
|
+
// code rather than a copy that can drift.
|
|
7
|
+
export * from "./primitives.js";
|
|
8
|
+
export * from "./verify.js";
|
|
9
|
+
export * from "./store.js";
|
|
10
|
+
export * from "./verification.js";
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/identity/index.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,6EAA6E;AAC7E,uEAAuE;AACvE,+EAA+E;AAC/E,6EAA6E;AAC7E,0CAA0C;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Deterministic JSON: keys sorted, no insignificant whitespace, UTF-8. */
|
|
2
|
+
export declare function canonicalize(value: unknown): string;
|
|
3
|
+
export declare function b64url(bytes: Uint8Array): string;
|
|
4
|
+
export declare function fromB64url(s: string): Uint8Array;
|
|
5
|
+
/** base64url(sha256(pub))[:16] — stable short id for a public key. */
|
|
6
|
+
export declare function keyId(publicKey: Uint8Array): string;
|
|
7
|
+
export declare function sha256Hex(text: string): string;
|
|
8
|
+
/** Raw base64url public key for a 32-byte Ed25519 seed. */
|
|
9
|
+
export declare function publicKeyB64urlFromSeed(seed: Uint8Array): string;
|
|
10
|
+
/** Sign the canonical form of `payload` with a 32-byte Ed25519 seed. */
|
|
11
|
+
export declare function signCanonical(payload: unknown, seed: Uint8Array): string;
|
|
12
|
+
/** Verify `sig` over the canonical form of `payload`. Never throws. */
|
|
13
|
+
export declare function verifyCanonical(payload: unknown, sig: string, publicKey: Uint8Array): boolean;
|
|
14
|
+
/** Structure an existing operator key signs to endorse a NEW operator key. */
|
|
15
|
+
export declare function endorsementPayload(fleetId: string, newKeyId: string, newPublicKeyB64url: string): {
|
|
16
|
+
v: number;
|
|
17
|
+
t: string;
|
|
18
|
+
fleet_id: string;
|
|
19
|
+
key_id: string;
|
|
20
|
+
public_key: string;
|
|
21
|
+
};
|
|
22
|
+
/** Structure an operator key signs to REVOKE an operator key (#27).
|
|
23
|
+
* Revocation mutates the trust root exactly as adoption does, so it needs the
|
|
24
|
+
* same proof: an unsigned relay `revoked:true` flag is advisory only. */
|
|
25
|
+
export declare function revocationPayload(fleetId: string, revokedKeyId: string, revokedAt: string): {
|
|
26
|
+
v: number;
|
|
27
|
+
t: string;
|
|
28
|
+
fleet_id: string;
|
|
29
|
+
key_id: string;
|
|
30
|
+
revoked_at: string;
|
|
31
|
+
};
|
|
32
|
+
/** Structure an operator key signs to UN-REVOKE a key (#27 / #48).
|
|
33
|
+
* Binds the revocation being undone, issue time, and a nonce so a captured
|
|
34
|
+
* un-revoke cannot clear a later tombstone. Never re-pins. */
|
|
35
|
+
export declare function unrevokePayload(fleetId: string, revokedKeyId: string, revokedAtBeingCleared: string, issuedAt: string, nonce: string): {
|
|
36
|
+
v: number;
|
|
37
|
+
t: string;
|
|
38
|
+
fleet_id: string;
|
|
39
|
+
key_id: string;
|
|
40
|
+
revoked_at_being_cleared: string;
|
|
41
|
+
issued_at: string;
|
|
42
|
+
nonce: string;
|
|
43
|
+
};
|
|
44
|
+
/** Structure the operator signs to endorse an AGENT's identity key (peer trust). */
|
|
45
|
+
export declare function agentKeyEndorsementPayload(fleetId: string, agentId: string, agentKeyId: string, agentPublicKeyB64url: string): {
|
|
46
|
+
v: number;
|
|
47
|
+
t: string;
|
|
48
|
+
fleet_id: string;
|
|
49
|
+
agent_id: string;
|
|
50
|
+
key_id: string;
|
|
51
|
+
public_key: string;
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=primitives.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../../../src/identity/primitives.ts"],"names":[],"mappings":"AAQA,2EAA2E;AAC3E,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAMnD;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAEhD;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAEhD;AAsBD,sEAAsE;AACtE,wBAAgB,KAAK,CAAC,SAAS,EAAE,UAAU,GAAG,MAAM,CAEnD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,2DAA2D;AAC3D,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAGhE;AAED,wEAAwE;AACxE,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAGxE;AAED,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,GAAG,OAAO,CAW7F;AAED,8EAA8E;AAC9E,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM;;;;;;EAQ/F;AAED;;0EAE0E;AAC1E,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;;;;;;EAQzF;AAED;;+DAE+D;AAC/D,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,qBAAqB,EAAE,MAAM,EAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM;;;;;;;;EAcd;AAED,oFAAoF;AACpF,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,oBAAoB,EAAE,MAAM;;;;;;;EAU7B"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Verifiable identity core shared by every TypeScript Ekho agent (OpenClaw plugin, MCP connector) — Node built-in crypto, so no
|
|
2
|
+
// ESM-only dependency under NodeNext. Its canonical serialization MUST match the
|
|
3
|
+
// TS relay (noble) and the Python plugin (cryptography) byte-for-byte; the frozen
|
|
4
|
+
// interop vector (packages/relay/tests/fixtures/operator-identity-vector.json)
|
|
5
|
+
// pins that contract.
|
|
6
|
+
import crypto from "node:crypto";
|
|
7
|
+
/** Deterministic JSON: keys sorted, no insignificant whitespace, UTF-8. */
|
|
8
|
+
export function canonicalize(value) {
|
|
9
|
+
if (value === null || typeof value !== "object")
|
|
10
|
+
return JSON.stringify(value);
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return "[" + value.map(canonicalize).join(",") + "]";
|
|
13
|
+
const obj = value;
|
|
14
|
+
const keys = Object.keys(obj).sort();
|
|
15
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k])).join(",") + "}";
|
|
16
|
+
}
|
|
17
|
+
export function b64url(bytes) {
|
|
18
|
+
return Buffer.from(bytes).toString("base64url");
|
|
19
|
+
}
|
|
20
|
+
export function fromB64url(s) {
|
|
21
|
+
return new Uint8Array(Buffer.from(s, "base64url"));
|
|
22
|
+
}
|
|
23
|
+
// Standard DER prefixes for raw 32-byte Ed25519 keys.
|
|
24
|
+
const PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
25
|
+
const SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
26
|
+
function privFromSeed(seed) {
|
|
27
|
+
return crypto.createPrivateKey({
|
|
28
|
+
key: Buffer.concat([PKCS8_PREFIX, Buffer.from(seed)]),
|
|
29
|
+
format: "der",
|
|
30
|
+
type: "pkcs8",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function pubFromRaw(publicKey) {
|
|
34
|
+
return crypto.createPublicKey({
|
|
35
|
+
key: Buffer.concat([SPKI_PREFIX, Buffer.from(publicKey)]),
|
|
36
|
+
format: "der",
|
|
37
|
+
type: "spki",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** base64url(sha256(pub))[:16] — stable short id for a public key. */
|
|
41
|
+
export function keyId(publicKey) {
|
|
42
|
+
return b64url(crypto.createHash("sha256").update(Buffer.from(publicKey)).digest()).slice(0, 16);
|
|
43
|
+
}
|
|
44
|
+
export function sha256Hex(text) {
|
|
45
|
+
return crypto.createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
|
|
46
|
+
}
|
|
47
|
+
/** Raw base64url public key for a 32-byte Ed25519 seed. */
|
|
48
|
+
export function publicKeyB64urlFromSeed(seed) {
|
|
49
|
+
const jwk = crypto.createPublicKey(privFromSeed(seed)).export({ format: "jwk" });
|
|
50
|
+
return jwk.x; // JWK OKP x is already base64url-encoded raw public key
|
|
51
|
+
}
|
|
52
|
+
/** Sign the canonical form of `payload` with a 32-byte Ed25519 seed. */
|
|
53
|
+
export function signCanonical(payload, seed) {
|
|
54
|
+
const sig = crypto.sign(null, Buffer.from(canonicalize(payload), "utf8"), privFromSeed(seed));
|
|
55
|
+
return Buffer.from(sig).toString("base64url");
|
|
56
|
+
}
|
|
57
|
+
/** Verify `sig` over the canonical form of `payload`. Never throws. */
|
|
58
|
+
export function verifyCanonical(payload, sig, publicKey) {
|
|
59
|
+
try {
|
|
60
|
+
return crypto.verify(null, Buffer.from(canonicalize(payload), "utf8"), pubFromRaw(publicKey), Buffer.from(sig, "base64url"));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Structure an existing operator key signs to endorse a NEW operator key. */
|
|
67
|
+
export function endorsementPayload(fleetId, newKeyId, newPublicKeyB64url) {
|
|
68
|
+
return {
|
|
69
|
+
v: 1,
|
|
70
|
+
t: "op-key-endorsement",
|
|
71
|
+
fleet_id: fleetId,
|
|
72
|
+
key_id: newKeyId,
|
|
73
|
+
public_key: newPublicKeyB64url,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Structure an operator key signs to REVOKE an operator key (#27).
|
|
77
|
+
* Revocation mutates the trust root exactly as adoption does, so it needs the
|
|
78
|
+
* same proof: an unsigned relay `revoked:true` flag is advisory only. */
|
|
79
|
+
export function revocationPayload(fleetId, revokedKeyId, revokedAt) {
|
|
80
|
+
return {
|
|
81
|
+
v: 1,
|
|
82
|
+
t: "op-key-revocation",
|
|
83
|
+
fleet_id: fleetId,
|
|
84
|
+
key_id: revokedKeyId,
|
|
85
|
+
revoked_at: revokedAt,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Structure an operator key signs to UN-REVOKE a key (#27 / #48).
|
|
89
|
+
* Binds the revocation being undone, issue time, and a nonce so a captured
|
|
90
|
+
* un-revoke cannot clear a later tombstone. Never re-pins. */
|
|
91
|
+
export function unrevokePayload(fleetId, revokedKeyId, revokedAtBeingCleared, issuedAt, nonce) {
|
|
92
|
+
if (!nonce) {
|
|
93
|
+
throw new Error("unrevoke nonce must be a non-empty string");
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
v: 1,
|
|
97
|
+
t: "op-key-unrevoke",
|
|
98
|
+
fleet_id: fleetId,
|
|
99
|
+
key_id: revokedKeyId,
|
|
100
|
+
revoked_at_being_cleared: revokedAtBeingCleared,
|
|
101
|
+
issued_at: issuedAt,
|
|
102
|
+
nonce,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Structure the operator signs to endorse an AGENT's identity key (peer trust). */
|
|
106
|
+
export function agentKeyEndorsementPayload(fleetId, agentId, agentKeyId, agentPublicKeyB64url) {
|
|
107
|
+
return {
|
|
108
|
+
v: 1,
|
|
109
|
+
t: "agent-key-endorsement",
|
|
110
|
+
fleet_id: fleetId,
|
|
111
|
+
agent_id: agentId,
|
|
112
|
+
key_id: agentKeyId,
|
|
113
|
+
public_key: agentPublicKeyB64url,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=primitives.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"primitives.js","sourceRoot":"","sources":["../../../src/identity/primitives.ts"],"names":[],"mappings":"AAAA,gIAAgI;AAChI,iFAAiF;AACjF,kFAAkF;AAClF,+EAA+E;AAC/E,sBAAsB;AAEtB,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,2EAA2E;AAC3E,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC9E,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IAC/E,MAAM,GAAG,GAAG,KAAgC,CAAC;IAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACrC,OAAO,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;AAC/F,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,KAAiB;IACtC,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,CAAS;IAClC,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC;AACrD,CAAC;AAED,sDAAsD;AACtD,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,kCAAkC,EAAE,KAAK,CAAC,CAAC;AAC5E,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,0BAA0B,EAAE,KAAK,CAAC,CAAC;AAEnE,SAAS,YAAY,CAAC,IAAgB;IACpC,OAAO,MAAM,CAAC,gBAAgB,CAAC;QAC7B,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACrD,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,OAAO;KACd,CAAC,CAAC;AACL,CAAC;AAED,SAAS,UAAU,CAAC,SAAqB;IACvC,OAAO,MAAM,CAAC,eAAe,CAAC;QAC5B,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;QACzD,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,MAAM;KACb,CAAC,CAAC;AACL,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,KAAK,CAAC,SAAqB;IACzC,OAAO,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAClG,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACrF,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,uBAAuB,CAAC,IAAgB;IACtD,MAAM,GAAG,GAAG,MAAM,CAAC,eAAe,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAkB,CAAC;IAClG,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,wDAAwD;AACxE,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,aAAa,CAAC,OAAgB,EAAE,IAAgB;IAC9D,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;IAC9F,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAChD,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,eAAe,CAAC,OAAgB,EAAE,GAAW,EAAE,SAAqB;IAClF,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,MAAM,CAClB,IAAI,EACJ,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,EAC1C,UAAU,CAAC,SAAS,CAAC,EACrB,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAC9B,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,kBAAkB,CAAC,OAAe,EAAE,QAAgB,EAAE,kBAA0B;IAC9F,OAAO;QACL,CAAC,EAAE,CAAC;QACJ,CAAC,EAAE,oBAAoB;QACvB,QAAQ,EAAE,OAAO;QACjB,MAAM,EAAE,QAAQ;QAChB,UAAU,EAAE,kBAAkB;KAC/B,CAAC;AACJ,CAAC;AAED;;0EAE0E;AAC1E,MAAM,UAAU,iBAAiB,CAAC,OAAe,EAAE,YAAoB,EAAE,SAAiB;IACxF,OAAO;QACL,CAAC,EAAE,CAAC;QACJ,CAAC,EAAE,mBAAmB;QACtB,QAAQ,EAAE,OAAO;QACjB,MAAM,EAAE,YAAY;QACpB,UAAU,EAAE,SAAS;KACtB,CAAC;AACJ,CAAC;AAED;;+DAE+D;AAC/D,MAAM,UAAU,eAAe,CAC7B,OAAe,EACf,YAAoB,EACpB,qBAA6B,EAC7B,QAAgB,EAChB,KAAa;IAEb,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO;QACL,CAAC,EAAE,CAAC;QACJ,CAAC,EAAE,iBAAiB;QACpB,QAAQ,EAAE,OAAO;QACjB,MAAM,EAAE,YAAY;QACpB,wBAAwB,EAAE,qBAAqB;QAC/C,SAAS,EAAE,QAAQ;QACnB,KAAK;KACN,CAAC;AACJ,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,0BAA0B,CACxC,OAAe,EACf,OAAe,EACf,UAAkB,EAClB,oBAA4B;IAE5B,OAAO;QACL,CAAC,EAAE,CAAC;QACJ,CAAC,EAAE,uBAAuB;QAC1B,QAAQ,EAAE,OAAO;QACjB,QAAQ,EAAE,OAAO;QACjB,MAAM,EAAE,UAAU;QAClB,UAAU,EAAE,oBAAoB;KACjC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export declare const IDENTITY_FILE = ".ekho-identity.json";
|
|
2
|
+
/** Why a pinned operator key is trusted on THIS box (#26). Written at the moment
|
|
3
|
+
* the gate admitted the key, so the question "why is this key trusted here?"
|
|
4
|
+
* has an offline answer — no relay round trip, and nothing to re-ask the relay
|
|
5
|
+
* for (which is exactly the party we don't trust). For a chain admission the
|
|
6
|
+
* endorsement signature is kept verbatim, so the endorsement can be re-verified
|
|
7
|
+
* from disk against the endorser's pinned public key. */
|
|
8
|
+
export interface OperatorKeyAdmission {
|
|
9
|
+
admitted_by: "tofu" | "chain";
|
|
10
|
+
/** Chain only: the pinned key whose endorsement admitted this one. */
|
|
11
|
+
endorsed_by_key_id?: string;
|
|
12
|
+
/** Chain only: the endorsement signature this box actually verified. */
|
|
13
|
+
endorsement_sig?: string;
|
|
14
|
+
admitted_at: string;
|
|
15
|
+
}
|
|
16
|
+
/** The agent's own Ed25519 identity (private seed) + the operator keys it pins. */
|
|
17
|
+
export interface EkhoIdentity {
|
|
18
|
+
seedHex: string;
|
|
19
|
+
pinnedOperatorKeys: Record<string, string>;
|
|
20
|
+
/** Set once, when the empty pin set trust-on-first-use adopted the relay's
|
|
21
|
+
* operator keys (#5). Latched forever so a later emptied pin set can never
|
|
22
|
+
* be re-seeded by whoever controls the relay at that moment. */
|
|
23
|
+
tofuAt?: string;
|
|
24
|
+
/** key_id -> ISO timestamp we first saw the relay report it revoked (#14).
|
|
25
|
+
* A tombstone ledger, not a cache: unpinning a revoked key is worthless on
|
|
26
|
+
* its own because the config seed, TOFU and endorsement chaining all re-add
|
|
27
|
+
* it on the next wake. Every add path consults this, so revocation sticks. */
|
|
28
|
+
revokedOperatorKeys?: Record<string, string>;
|
|
29
|
+
/** key_id -> the evidence that admitted it (#26). */
|
|
30
|
+
operatorKeyAdmissions?: Record<string, OperatorKeyAdmission>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Thrown instead of minting. Before 4 Oct 2026 an identity file that was
|
|
34
|
+
* present but unparseable, or absent on a box whose config already names an
|
|
35
|
+
* enrolled agent, silently produced a NEW random seed and registered it with
|
|
36
|
+
* the relay. Seven live "Jarvis" identity keys with no private half anywhere
|
|
37
|
+
* were found that way, each later endorsed by the operator in good faith. A
|
|
38
|
+
* key that exists only because a read failed is a forged identity, not a
|
|
39
|
+
* recovery; the caller must stop and say so.
|
|
40
|
+
*/
|
|
41
|
+
export declare class IdentityUnavailableError extends Error {
|
|
42
|
+
readonly reason: "unreadable" | "missing";
|
|
43
|
+
constructor(message: string, reason: "unreadable" | "missing");
|
|
44
|
+
}
|
|
45
|
+
/** Set to "1" to let an already-enrolled agent mint a brand-new identity key. */
|
|
46
|
+
export declare const ALLOW_NEW_IDENTITY_ENV = "EKHO_ALLOW_NEW_IDENTITY";
|
|
47
|
+
export interface LoadIdentityOptions {
|
|
48
|
+
/**
|
|
49
|
+
* Whether an ABSENT file may be answered with a freshly minted identity.
|
|
50
|
+
* Default true (first enrolment). Pass false for an agent whose config already
|
|
51
|
+
* carries enrolled credentials: its identity must already exist somewhere,
|
|
52
|
+
* so an absent file is a lost/moved file, not a new agent.
|
|
53
|
+
*/
|
|
54
|
+
allowCreate?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export declare function loadOrCreateIdentity(configDir: string, opts?: LoadIdentityOptions): EkhoIdentity;
|
|
57
|
+
/** Byte-for-byte copy of an unusable state file, for forensics. Bytes, not a
|
|
58
|
+
* decoded string: invalid UTF-8 must survive exactly (review, Case, 4 Oct). */
|
|
59
|
+
export declare function preserveUnusableFile(filePath: string, raw: Buffer | undefined): string | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Atomic: write a sibling temp file and rename it over the real one, so no
|
|
62
|
+
* reader (another plugin instance, a CLI run, the other runtime sharing this
|
|
63
|
+
* dir) can ever observe a truncated identity file mid-write.
|
|
64
|
+
*/
|
|
65
|
+
export declare function saveIdentity(configDir: string, identity: EkhoIdentity): void;
|
|
66
|
+
export declare function identityPublicKey(identity: EkhoIdentity): string;
|
|
67
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/identity/store.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,aAAa,wBAAwB,CAAC;AAEnD;;;;;0DAK0D;AAC1D,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,sEAAsE;IACtE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wEAAwE;IACxE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,mFAAmF;AACnF,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C;;qEAEiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;mFAG+E;IAC/E,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,qDAAqD;IACrD,qBAAqB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;CAC9D;AAED;;;;;;;;GAQG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IAG/C,QAAQ,CAAC,MAAM,EAAE,YAAY,GAAG,SAAS;gBADzC,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,YAAY,GAAG,SAAS;CAK5C;AAED,iFAAiF;AACjF,eAAO,MAAM,sBAAsB,4BAA4B,CAAC;AAEhE,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,mBAAwB,GAAG,YAAY,CAoDpG;AAED;gFACgF;AAChF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CASlG;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,QAerE;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,CAEhE"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// The agent's own Ed25519 identity file: load, refuse-to-mint rules, atomic
|
|
2
|
+
// save. Shared by every TypeScript Ekho agent; the OpenClaw plugin re-exports
|
|
3
|
+
// it from its credentials module so its tests and callers are unchanged.
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { publicKeyB64urlFromSeed } from "./primitives.js";
|
|
8
|
+
export const IDENTITY_FILE = ".ekho-identity.json";
|
|
9
|
+
/**
|
|
10
|
+
* Thrown instead of minting. Before 4 Oct 2026 an identity file that was
|
|
11
|
+
* present but unparseable, or absent on a box whose config already names an
|
|
12
|
+
* enrolled agent, silently produced a NEW random seed and registered it with
|
|
13
|
+
* the relay. Seven live "Jarvis" identity keys with no private half anywhere
|
|
14
|
+
* were found that way, each later endorsed by the operator in good faith. A
|
|
15
|
+
* key that exists only because a read failed is a forged identity, not a
|
|
16
|
+
* recovery; the caller must stop and say so.
|
|
17
|
+
*/
|
|
18
|
+
export class IdentityUnavailableError extends Error {
|
|
19
|
+
reason;
|
|
20
|
+
constructor(message, reason) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.reason = reason;
|
|
23
|
+
this.name = "IdentityUnavailableError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Set to "1" to let an already-enrolled agent mint a brand-new identity key. */
|
|
27
|
+
export const ALLOW_NEW_IDENTITY_ENV = "EKHO_ALLOW_NEW_IDENTITY";
|
|
28
|
+
export function loadOrCreateIdentity(configDir, opts = {}) {
|
|
29
|
+
const filePath = path.join(configDir, IDENTITY_FILE);
|
|
30
|
+
if (fs.existsSync(filePath)) {
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
raw = fs.readFileSync(filePath);
|
|
34
|
+
const data = JSON.parse(raw.toString("utf-8"));
|
|
35
|
+
if (data?.seedHex) {
|
|
36
|
+
// Spread the file FIRST: a field this build doesn't know about (written
|
|
37
|
+
// by a newer plugin, or by the other runtime sharing the config dir)
|
|
38
|
+
// survives the load/save round trip instead of being silently dropped.
|
|
39
|
+
return {
|
|
40
|
+
...data,
|
|
41
|
+
seedHex: String(data.seedHex),
|
|
42
|
+
pinnedOperatorKeys: data.pinnedOperatorKeys ?? {},
|
|
43
|
+
...(data.tofuAt ? { tofuAt: String(data.tofuAt) } : {}),
|
|
44
|
+
...(data.revokedOperatorKeys
|
|
45
|
+
? { revokedOperatorKeys: data.revokedOperatorKeys }
|
|
46
|
+
: {}),
|
|
47
|
+
...(data.operatorKeyAdmissions
|
|
48
|
+
? { operatorKeyAdmissions: data.operatorKeyAdmissions }
|
|
49
|
+
: {})
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* present but unusable: handled below, never regenerated over */
|
|
55
|
+
}
|
|
56
|
+
// The file is there but this build cannot use it (unreadable, not JSON,
|
|
57
|
+
// or no seed). Keep the bytes for forensics and refuse. Overwriting them
|
|
58
|
+
// with a new seed would both destroy the evidence and mint a second live
|
|
59
|
+
// identity for this agent on the relay.
|
|
60
|
+
const preserved = preserveUnusableFile(filePath, raw);
|
|
61
|
+
throw new IdentityUnavailableError(`[ekho] identity file ${filePath} is present but unusable; refusing to mint a replacement key` +
|
|
62
|
+
(preserved ? ` (bytes preserved at ${preserved})` : "") +
|
|
63
|
+
`. Restore the file from a backup, or remove it and set ${ALLOW_NEW_IDENTITY_ENV}=1 to enrol a new key on purpose.`, "unreadable");
|
|
64
|
+
}
|
|
65
|
+
if (opts.allowCreate === false) {
|
|
66
|
+
throw new IdentityUnavailableError(`[ekho] no identity file at ${filePath} but this agent is already enrolled; refusing to mint a new key. ` +
|
|
67
|
+
`Restore the file from a backup (the relay already holds this agent's key), or set ${ALLOW_NEW_IDENTITY_ENV}=1 to re-key on purpose.`, "missing");
|
|
68
|
+
}
|
|
69
|
+
const identity = {
|
|
70
|
+
seedHex: crypto.randomBytes(32).toString("hex"),
|
|
71
|
+
pinnedOperatorKeys: {}
|
|
72
|
+
};
|
|
73
|
+
saveIdentity(configDir, identity);
|
|
74
|
+
return identity;
|
|
75
|
+
}
|
|
76
|
+
/** Byte-for-byte copy of an unusable state file, for forensics. Bytes, not a
|
|
77
|
+
* decoded string: invalid UTF-8 must survive exactly (review, Case, 4 Oct). */
|
|
78
|
+
export function preserveUnusableFile(filePath, raw) {
|
|
79
|
+
if (raw === undefined)
|
|
80
|
+
return undefined;
|
|
81
|
+
const target = `${filePath}.unusable-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
82
|
+
try {
|
|
83
|
+
fs.writeFileSync(target, raw, { mode: 0o600 });
|
|
84
|
+
return target;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return undefined; // best effort; the refusal is what matters
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Atomic: write a sibling temp file and rename it over the real one, so no
|
|
92
|
+
* reader (another plugin instance, a CLI run, the other runtime sharing this
|
|
93
|
+
* dir) can ever observe a truncated identity file mid-write.
|
|
94
|
+
*/
|
|
95
|
+
export function saveIdentity(configDir, identity) {
|
|
96
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
97
|
+
const filePath = path.join(configDir, IDENTITY_FILE);
|
|
98
|
+
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
99
|
+
fs.writeFileSync(tmp, JSON.stringify(identity, null, 2), { mode: 0o600 });
|
|
100
|
+
try {
|
|
101
|
+
fs.renameSync(tmp, filePath);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
try {
|
|
105
|
+
fs.unlinkSync(tmp);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
/* nothing to clean */
|
|
109
|
+
}
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function identityPublicKey(identity) {
|
|
114
|
+
return publicKeyB64urlFromSeed(new Uint8Array(Buffer.from(identity.seedHex, "hex")));
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/identity/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { type RosterEntryLike, type SignedMessage, type VerifyResult } from "./verify.js";
|
|
2
|
+
import { type EkhoIdentity } from "./store.js";
|
|
3
|
+
export interface OperatorKeyEntryLike {
|
|
4
|
+
key_id?: string;
|
|
5
|
+
public_key?: string;
|
|
6
|
+
revoked?: boolean;
|
|
7
|
+
endorsed_by_key_id?: string | null;
|
|
8
|
+
endorsement_sig?: string | null;
|
|
9
|
+
/** #27: signature over revocationPayload(fleet, key_id, revoked_at) by a
|
|
10
|
+
* currently pinned operator key. WITHOUT it, `revoked` is advisory only. */
|
|
11
|
+
revocation_sig?: string | null;
|
|
12
|
+
/** #27: the revocation time, inside the signed bytes (so the relay cannot
|
|
13
|
+
* restate when a key died under a still-valid signature). */
|
|
14
|
+
revoked_at?: string | null;
|
|
15
|
+
/** #27 / #48: signature over unrevokePayload(fleet, key, revoked_at, issued_at, nonce). */
|
|
16
|
+
unrevoke_sig?: string | null;
|
|
17
|
+
unrevoke_revoked_at?: string | null;
|
|
18
|
+
unrevoke_issued_at?: string | null;
|
|
19
|
+
unrevoke_nonce?: string | null;
|
|
20
|
+
}
|
|
21
|
+
/** Where the sync reports trust-root decisions. Defaults to the console, because
|
|
22
|
+
* a relay claiming a revocation it can't prove must never pass in silence. */
|
|
23
|
+
export interface SyncLog {
|
|
24
|
+
info?: (...a: unknown[]) => void;
|
|
25
|
+
warn?: (...a: unknown[]) => void;
|
|
26
|
+
}
|
|
27
|
+
/** How many identical advisory polls to swallow before a reminder. ~5s poll
|
|
28
|
+
* → a reminder about every 10 minutes, not 150k lines in a day and a half. */
|
|
29
|
+
export declare const ADVISORY_REVOCATION_WARNING_SUMMARY_EVERY = 120;
|
|
30
|
+
/** Process-local cap on identity+fleet throttle scopes. Eviction re-emits that
|
|
31
|
+
* scope's next first-seen warning; it cannot mute a different live scope. */
|
|
32
|
+
export declare const ADVISORY_REVOCATION_WARNING_MAX_SCOPES = 64;
|
|
33
|
+
/** Test-only isolation for the process-local Map. Not plugin runtime API. */
|
|
34
|
+
export declare function resetAdvisoryRevocationWarningStateForTests(): void;
|
|
35
|
+
/** Update pinned operator keys from the inbox. Adds a new key ONLY if endorsed by
|
|
36
|
+
* an already-pinned key; removes one ONLY on a SIGNED revocation. Returns true
|
|
37
|
+
* if the identity changed (and so must be persisted).
|
|
38
|
+
*
|
|
39
|
+
* Trust mutates in one direction only, and never on the relay's say-so (#27).
|
|
40
|
+
* An unsigned `revoked: true` is a HINT: the key is skipped for new adoption
|
|
41
|
+
* and the claim is logged, but nothing is written and nothing is unpinned.
|
|
42
|
+
* Treating it as authoritative (the #14 regression) meant one poll from a
|
|
43
|
+
* compromised relay could tombstone and unpin an entire fleet's trust root,
|
|
44
|
+
* permanently — the tombstone survives restarts by design, so the damage was
|
|
45
|
+
* not even recoverable by restarting against an honest relay.
|
|
46
|
+
*
|
|
47
|
+
* One deliberate exception to "no relay TOFU" (#5): an identity that has never
|
|
48
|
+
* pinned ANY key can't grow a chain — endorsements need an already-pinned root,
|
|
49
|
+
* so verification stayed dormant forever on every agent nobody hand-configured
|
|
50
|
+
* (the Aug 2026 silent-drop incident). First contact with an empty pin set
|
|
51
|
+
* adopts the relay's current non-revoked keys as the trust root, exactly once
|
|
52
|
+
* (tofuAt latches). Enrollment already trusts the relay this much — it accepts
|
|
53
|
+
* the shared secret over the same channel. Pre-pinning via config/env skips
|
|
54
|
+
* TOFU entirely and stays the stronger option. */
|
|
55
|
+
export declare function syncPinnedOperatorKeys(identity: EkhoIdentity, operatorKeys: OperatorKeyEntryLike[], fleetId: string | null | undefined, log?: SyncLog): boolean;
|
|
56
|
+
export declare function verifyBatch(messages: SignedMessage[], opts: {
|
|
57
|
+
identity: EkhoIdentity;
|
|
58
|
+
selfAgentId: string;
|
|
59
|
+
fleetId: string | null | undefined;
|
|
60
|
+
roster: RosterEntryLike[];
|
|
61
|
+
seenNonces: Set<string>;
|
|
62
|
+
now: Date;
|
|
63
|
+
}): Record<string, VerifyResult | null>;
|
|
64
|
+
/** How strictly peer messages must prove themselves before waking a turn (#5).
|
|
65
|
+
* - "warn" (default): unsigned/unverifiable peers still wake, but are rendered
|
|
66
|
+
* untrusted — the graceful relay-attested fallback, named for what it is.
|
|
67
|
+
* - "require": a peer message wakes ONLY when signed and verified; anything
|
|
68
|
+
* else is dead-lettered by the tick. Operator messages keep the explicit
|
|
69
|
+
* relay-attested operator_trusted toggle as their unsigned fallback — that
|
|
70
|
+
* flag is operator-set, not relay-implied, so "require" doesn't cut the
|
|
71
|
+
* operator off on consoles that don't sign yet.
|
|
72
|
+
* - "off": same wake behavior as "warn" (kept distinct for config clarity). */
|
|
73
|
+
export type RequireSignedMode = "off" | "warn" | "require";
|
|
74
|
+
export declare function parseRequireSignedMode(raw: string | undefined | null): RequireSignedMode;
|
|
75
|
+
/** The execution-authority gate. Signed → act iff verified; signed-but-invalid →
|
|
76
|
+
* blocked; unsigned → per requireSigned mode (see above). */
|
|
77
|
+
export declare function shouldAutowake(msg: SignedMessage, verification: VerifyResult | null | undefined, operatorTrusted: boolean, peerEnabled: boolean, requireSigned?: RequireSignedMode): boolean;
|
|
78
|
+
/** Sign an outbound peer message so recipients can verify it came from us.
|
|
79
|
+
*
|
|
80
|
+
* v2 (#9) extends coverage to message_type, priority and the attachment ids —
|
|
81
|
+
* previously a compromised relay could relabel a message (direct → alert) or
|
|
82
|
+
* swap its attachments under a still-valid signature. Compatibility is free in
|
|
83
|
+
* both directions: verifiers check the signature over the WHOLE canonical and
|
|
84
|
+
* only bind the fields they know, so old verifiers accept v2 envelopes and new
|
|
85
|
+
* verifiers enforce the extra bindings only when the envelope declares v>=2. */
|
|
86
|
+
export declare function buildSignedSendFields(opts: {
|
|
87
|
+
identity: EkhoIdentity;
|
|
88
|
+
fleetId: string;
|
|
89
|
+
selfAgentId: string;
|
|
90
|
+
recipient: Record<string, unknown>;
|
|
91
|
+
conversationId: string;
|
|
92
|
+
bodyText: string;
|
|
93
|
+
nonce: string;
|
|
94
|
+
sentAt: string;
|
|
95
|
+
messageType: string;
|
|
96
|
+
priority: string;
|
|
97
|
+
attachments?: string[];
|
|
98
|
+
}): {
|
|
99
|
+
agent_sig: string;
|
|
100
|
+
key_id: string;
|
|
101
|
+
sig_canonical: Record<string, unknown>;
|
|
102
|
+
};
|
|
103
|
+
/** A quoted snapshot of another message — a reply target (`reply_to`) or a
|
|
104
|
+
* conversation-history / floor-tail entry. Since #43 the relay copies the
|
|
105
|
+
* signature fields of the message it quotes onto the snapshot, so a snapshot
|
|
106
|
+
* can be checked rather than merely inspected. Note the shape difference from
|
|
107
|
+
* an inbox message: a snapshot carries `text` at the top level, not `body.text`. */
|
|
108
|
+
export interface MsgSnapshotLike {
|
|
109
|
+
message_id?: string;
|
|
110
|
+
sender_agent_id?: string;
|
|
111
|
+
sender_kind?: "operator" | "agent";
|
|
112
|
+
text?: string;
|
|
113
|
+
message_type?: string;
|
|
114
|
+
priority?: string;
|
|
115
|
+
attachments?: string[];
|
|
116
|
+
operator_sig?: string | null;
|
|
117
|
+
agent_sig?: string | null;
|
|
118
|
+
key_id?: string | null;
|
|
119
|
+
sig_canonical?: Record<string, unknown> | null;
|
|
120
|
+
}
|
|
121
|
+
/** Verdict for one quoted snapshot: did its signature actually check out? */
|
|
122
|
+
export type SnapshotVerifier = (snapshot: MsgSnapshotLike | null | undefined) => boolean;
|
|
123
|
+
/** The fail-closed default (#20): when verification cannot run — no pinned
|
|
124
|
+
* operator keys, no fleet id, no identity — every snapshot is UNVERIFIED.
|
|
125
|
+
* Never "it has a signature field, so call it signed". */
|
|
126
|
+
export declare const NO_SNAPSHOT_VERIFICATION: SnapshotVerifier;
|
|
127
|
+
/**
|
|
128
|
+
* Build the snapshot verifier for one turn, from the same trust root the
|
|
129
|
+
* autowake path uses: this agent's pinned operator keys plus the batch roster.
|
|
130
|
+
*
|
|
131
|
+
* Runs the ordinary inbound verifier (`verifyInbound`) over a snapshot mapped
|
|
132
|
+
* into its `SignedMessage` shape — there is deliberately no second, laxer
|
|
133
|
+
* verifier for quoted text. Two consequences worth naming:
|
|
134
|
+
*
|
|
135
|
+
* - An older relay that omits `sig_canonical` (or the v2-bound `message_type` /
|
|
136
|
+
* `priority` / `attachments`) simply fails the checks, so its snapshots read
|
|
137
|
+
* UNVERIFIED. That is the intended direction to fail in.
|
|
138
|
+
* - The nonce set is fresh per turn rather than the live seen-nonce set. A
|
|
139
|
+
* quote is by definition a re-presentation of a message that may already
|
|
140
|
+
* have been delivered and had its nonce burned; the burn exists to stop a
|
|
141
|
+
* replayed message WAKING a turn, and a snapshot can never wake one. The
|
|
142
|
+
* staleness window is left at the verifier's default, so a snapshot replayed
|
|
143
|
+
* from beyond it still reads UNVERIFIED.
|
|
144
|
+
*/
|
|
145
|
+
export declare function makeSnapshotVerifier(opts: {
|
|
146
|
+
identity?: EkhoIdentity | null;
|
|
147
|
+
selfAgentId: string;
|
|
148
|
+
fleetId: string | null | undefined;
|
|
149
|
+
roster: RosterEntryLike[];
|
|
150
|
+
now: Date;
|
|
151
|
+
}): SnapshotVerifier;
|
|
152
|
+
//# sourceMappingURL=verification.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verification.d.ts","sourceRoot":"","sources":["../../../src/identity/verification.ts"],"names":[],"mappings":"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"}
|