@genkaku/protocol 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/bridge.d.ts +46 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +542 -0
- package/dist/chain.d.ts +496 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +763 -0
- package/dist/credit.d.ts +35 -0
- package/dist/credit.d.ts.map +1 -0
- package/dist/credit.js +112 -0
- package/dist/dial.d.ts +43 -0
- package/dist/dial.d.ts.map +1 -0
- package/dist/dial.js +266 -0
- package/dist/directory.d.ts +39 -0
- package/dist/directory.d.ts.map +1 -0
- package/dist/directory.js +149 -0
- package/dist/discovery.d.ts +20 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +58 -0
- package/dist/hello.d.ts +14 -0
- package/dist/hello.d.ts.map +1 -0
- package/dist/hello.js +42 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/keys.d.ts +30 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +126 -0
- package/dist/llmBridge.d.ts +34 -0
- package/dist/llmBridge.d.ts.map +1 -0
- package/dist/llmBridge.js +151 -0
- package/dist/lp.d.ts +31 -0
- package/dist/lp.d.ts.map +1 -0
- package/dist/lp.js +116 -0
- package/dist/modeManager.d.ts +30 -0
- package/dist/modeManager.d.ts.map +1 -0
- package/dist/modeManager.js +81 -0
- package/dist/netconfig.d.ts +11 -0
- package/dist/netconfig.d.ts.map +1 -0
- package/dist/netconfig.js +39 -0
- package/dist/offer.d.ts +70 -0
- package/dist/offer.d.ts.map +1 -0
- package/dist/offer.js +84 -0
- package/dist/ollama.d.ts +53 -0
- package/dist/ollama.d.ts.map +1 -0
- package/dist/ollama.js +162 -0
- package/dist/p2pNode.d.ts +50 -0
- package/dist/p2pNode.d.ts.map +1 -0
- package/dist/p2pNode.js +450 -0
- package/dist/paywall.d.ts +30 -0
- package/dist/paywall.d.ts.map +1 -0
- package/dist/paywall.js +84 -0
- package/dist/publishOptIn.d.ts +15 -0
- package/dist/publishOptIn.d.ts.map +1 -0
- package/dist/publishOptIn.js +34 -0
- package/dist/receipts.d.ts +61 -0
- package/dist/receipts.d.ts.map +1 -0
- package/dist/receipts.js +135 -0
- package/dist/registry.d.ts +19 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +50 -0
- package/dist/rpc.d.ts +24 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +143 -0
- package/dist/status.d.ts +56 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +354 -0
- package/dist/userSession.d.ts +64 -0
- package/dist/userSession.d.ts.map +1 -0
- package/dist/userSession.js +125 -0
- package/dist/workerSession.d.ts +67 -0
- package/dist/workerSession.d.ts.map +1 -0
- package/dist/workerSession.js +65 -0
- package/package.json +56 -3
- package/README.md +0 -3
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const DNS_SEED_NAME = "_genkaku-v0.seeds.genkaku.app";
|
|
2
|
+
/** Cloudflare's JSON DoH endpoint (no key, no logging of the query body). */
|
|
3
|
+
export declare const DEFAULT_DOH_URL = "https://cloudflare-dns.com/dns-query";
|
|
4
|
+
export type SeedResolverOptions = {
|
|
5
|
+
/** the DNS name to query (default _genkaku-v0.seeds.genkaku.app) */
|
|
6
|
+
name?: string;
|
|
7
|
+
/** DoH endpoint override (tests inject a fake fetch instead) */
|
|
8
|
+
dohUrl?: string;
|
|
9
|
+
/** injectable fetch for tests */
|
|
10
|
+
fetchImpl?: typeof fetch;
|
|
11
|
+
/** hard cap — discovery must never stall the marketplace */
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
};
|
|
14
|
+
/** Resolve TXT seed multiaddrs. Any failure (network, DNS, malformed record)
|
|
15
|
+
* degrades to [] — the caller falls through to peerstore/manual layers. */
|
|
16
|
+
export declare function resolveDnsSeeds(opts?: SeedResolverOptions): Promise<string[]>;
|
|
17
|
+
/** TXT payloads → multiaddrs. Quoted chunks are joined, `v=1;` style prefixes
|
|
18
|
+
* ignored, and only "/"-prefixed tokens survive. */
|
|
19
|
+
export declare function parseSeedTxt(records: string[]): string[];
|
|
20
|
+
//# sourceMappingURL=discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,aAAa,kCAAkC,CAAA;AAE5D,6EAA6E;AAC7E,eAAO,MAAM,eAAe,yCAAyC,CAAA;AAKrE,MAAM,MAAM,mBAAmB,GAAG;IAChC,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,iCAAiC;IACjC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;4EAC4E;AAC5E,wBAAsB,eAAe,CAAC,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA0BvF;AAED;qDACqD;AACrD,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAoBxD"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// GENKAKU: discovery layer 2 (P2 §2) — DNS TXT seeds.
|
|
2
|
+
// A TXT record lists multiaddrs; editing DNS reboots discovery without a
|
|
3
|
+
// client release. Resolution goes through DNS-over-HTTPS so a client whose
|
|
4
|
+
// local resolver strips TXT (or is hijacked) still works, and so there is no
|
|
5
|
+
// native resolver dependency in the main process.
|
|
6
|
+
//
|
|
7
|
+
// Record shape (chunked strings are joined before parsing):
|
|
8
|
+
// _genkaku-v0.seeds.genkaku.app. TXT "v=1; seeds=<addr1>,<addr2>"
|
|
9
|
+
// The `v=1;` prefix is optional; only tokens that look like multiaddrs ("/…")
|
|
10
|
+
// are kept, so prose or future fields cannot be dialed by accident.
|
|
11
|
+
export const DNS_SEED_NAME = '_genkaku-v0.seeds.genkaku.app';
|
|
12
|
+
/** Cloudflare's JSON DoH endpoint (no key, no logging of the query body). */
|
|
13
|
+
export const DEFAULT_DOH_URL = 'https://cloudflare-dns.com/dns-query';
|
|
14
|
+
/** Resolve TXT seed multiaddrs. Any failure (network, DNS, malformed record)
|
|
15
|
+
* degrades to [] — the caller falls through to peerstore/manual layers. */
|
|
16
|
+
export async function resolveDnsSeeds(opts = {}) {
|
|
17
|
+
const name = opts.name ?? DNS_SEED_NAME;
|
|
18
|
+
const dohUrl = opts.dohUrl ?? DEFAULT_DOH_URL;
|
|
19
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
20
|
+
const url = `${dohUrl}?name=${encodeURIComponent(name)}&type=TXT`;
|
|
21
|
+
try {
|
|
22
|
+
const res = await doFetch(url, {
|
|
23
|
+
headers: { accept: 'application/dns-json' },
|
|
24
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 4_000),
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const body = (await res.json());
|
|
30
|
+
if (body.Status !== 0 || !Array.isArray(body.Answer)) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return parseSeedTxt(body.Answer.map(a => a.data).filter((d) => typeof d === 'string'));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** TXT payloads → multiaddrs. Quoted chunks are joined, `v=1;` style prefixes
|
|
40
|
+
* ignored, and only "/"-prefixed tokens survive. */
|
|
41
|
+
export function parseSeedTxt(records) {
|
|
42
|
+
const out = new Set();
|
|
43
|
+
for (const raw of records) {
|
|
44
|
+
const joined = raw
|
|
45
|
+
.split('"')
|
|
46
|
+
.join('')
|
|
47
|
+
.replace(/^\s*v=\d+\s*;\s*/i, '');
|
|
48
|
+
for (const token of joined.split(/[,\s]+/)) {
|
|
49
|
+
// tolerate `seeds=` / `v=` style key prefixes on the first token
|
|
50
|
+
const addr = token.trim().replace(/^[a-z][a-z0-9_-]*=/i, '');
|
|
51
|
+
if (addr.startsWith('/') && addr.includes('/p2p/')) {
|
|
52
|
+
out.add(addr);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return [...out];
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=discovery.js.map
|
package/dist/hello.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type HelloProof = {
|
|
2
|
+
peerId: string;
|
|
3
|
+
wallet: `0x${string}`;
|
|
4
|
+
sig: string;
|
|
5
|
+
};
|
|
6
|
+
/** Fresh challenge for one handshake (never reused, never stored). */
|
|
7
|
+
export declare function newNonce(): `0x${string}`;
|
|
8
|
+
/** The exact bytes both sides sign — one definition, no drift. */
|
|
9
|
+
export declare function helloDigest(peerId: string, wallet: `0x${string}`, nonce: `0x${string}`): Uint8Array;
|
|
10
|
+
/** Worker side: prove possession of the wallet key for this PeerId. */
|
|
11
|
+
export declare function signHello(key: `0x${string}`, peerId: string, nonce: `0x${string}`): Promise<HelloProof>;
|
|
12
|
+
/** Client side: does this proof really come from `wallet` over `nonce`? */
|
|
13
|
+
export declare function verifyHello(proof: HelloProof, nonce: `0x${string}`): Promise<boolean>;
|
|
14
|
+
//# sourceMappingURL=hello.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hello.d.ts","sourceRoot":"","sources":["../src/hello.ts"],"names":[],"mappings":"AA2BA,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,KAAK,MAAM,EAAE,CAAA;IACrB,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,sEAAsE;AACtE,wBAAgB,QAAQ,IAAI,KAAK,MAAM,EAAE,CAExC;AAED,kEAAkE;AAClE,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,KAAK,MAAM,EAAE,EACrB,KAAK,EAAE,KAAK,MAAM,EAAE,GACnB,UAAU,CAEZ;AAED,uEAAuE;AACvE,wBAAsB,SAAS,CAC7B,GAAG,EAAE,KAAK,MAAM,EAAE,EAClB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,KAAK,MAAM,EAAE,GACnB,OAAO,CAAC,UAAU,CAAC,CAKrB;AAED,2EAA2E;AAC3E,wBAAsB,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAI3F"}
|
package/dist/hello.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// GENKAKU: data-plane identity proof (P4.9e).
|
|
2
|
+
//
|
|
3
|
+
// WorkerRegistry proves wallet ⇄ PeerId, but only for a worker that chose to
|
|
4
|
+
// register — and the record is self-declared (whoever pays gas claims the
|
|
5
|
+
// peerId). The hello handshake closes that hole directly: the client sends a
|
|
6
|
+
// fresh 32-byte nonce, the peer signs
|
|
7
|
+
//
|
|
8
|
+
// keccak("genkaku.hello.v0:<peerId>:<wallet>:<nonce>")
|
|
9
|
+
//
|
|
10
|
+
// with the EVM key it claims, and the client recovers the signer. A peer can
|
|
11
|
+
// only answer for a wallet whose private key it holds, so an offer that claims
|
|
12
|
+
// someone else's wallet is dropped before any escrow is opened. This also works
|
|
13
|
+
// for invite-link workers that never touched the chain.
|
|
14
|
+
//
|
|
15
|
+
// Both nonce and peerId are inside the digest: the nonce makes a recorded hello
|
|
16
|
+
// useless to another client, and the peerId stops a peer from re-serving a
|
|
17
|
+
// signature it obtained from a different node.
|
|
18
|
+
import { randomBytes } from 'node:crypto';
|
|
19
|
+
import { keccak256, toBytes } from 'viem';
|
|
20
|
+
import { walletAddress } from "./chain.js";
|
|
21
|
+
import { recoverSigner, signDigest } from "./receipts.js";
|
|
22
|
+
const HELLO_TAG = 'genkaku.hello.v0:';
|
|
23
|
+
/** Fresh challenge for one handshake (never reused, never stored). */
|
|
24
|
+
export function newNonce() {
|
|
25
|
+
return `0x${randomBytes(32).toString('hex')}`;
|
|
26
|
+
}
|
|
27
|
+
/** The exact bytes both sides sign — one definition, no drift. */
|
|
28
|
+
export function helloDigest(peerId, wallet, nonce) {
|
|
29
|
+
return toBytes(keccak256(toBytes(`${HELLO_TAG}${peerId}:${wallet.toLowerCase()}:${nonce.toLowerCase()}`)));
|
|
30
|
+
}
|
|
31
|
+
/** Worker side: prove possession of the wallet key for this PeerId. */
|
|
32
|
+
export async function signHello(key, peerId, nonce) {
|
|
33
|
+
const wallet = walletAddress(key);
|
|
34
|
+
const sig = await signDigest(key, helloDigest(peerId, wallet, nonce));
|
|
35
|
+
return { peerId, wallet, sig };
|
|
36
|
+
}
|
|
37
|
+
/** Client side: does this proof really come from `wallet` over `nonce`? */
|
|
38
|
+
export async function verifyHello(proof, nonce) {
|
|
39
|
+
const signer = await recoverSigner(helloDigest(proof.peerId, proof.wallet, nonce), proof.sig);
|
|
40
|
+
return signer != null && signer.toLowerCase() === proof.wallet.toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=hello.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenKaKu protocol core.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is harness-independent: contracts, escrow accounting, the
|
|
5
|
+
* libp2p transport, the Ollama supervisor and the OpenAI-compatible bridge.
|
|
6
|
+
* The DeepSeek Harness adapters live in @genkaku/dsh-runtime and the two
|
|
7
|
+
* plugin packages above it.
|
|
8
|
+
*/
|
|
9
|
+
export * from './bridge.ts';
|
|
10
|
+
export * from './chain.ts';
|
|
11
|
+
export * from './credit.ts';
|
|
12
|
+
export * from './dial.ts';
|
|
13
|
+
export * from './directory.ts';
|
|
14
|
+
export * from './discovery.ts';
|
|
15
|
+
export * from './hello.ts';
|
|
16
|
+
export * from './keys.ts';
|
|
17
|
+
export * from './llmBridge.ts';
|
|
18
|
+
export * from './lp.ts';
|
|
19
|
+
export * from './modeManager.ts';
|
|
20
|
+
export * from './netconfig.ts';
|
|
21
|
+
export * from './offer.ts';
|
|
22
|
+
export * from './ollama.ts';
|
|
23
|
+
export * from './p2pNode.ts';
|
|
24
|
+
export * from './paywall.ts';
|
|
25
|
+
export * from './publishOptIn.ts';
|
|
26
|
+
export * from './receipts.ts';
|
|
27
|
+
export * from './registry.ts';
|
|
28
|
+
export * from './rpc.ts';
|
|
29
|
+
export * from './status.ts';
|
|
30
|
+
export * from './userSession.ts';
|
|
31
|
+
export * from './workerSession.ts';
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,SAAS,CAAA;AACvB,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,aAAa,CAAA;AAC3B,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenKaKu protocol core.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is harness-independent: contracts, escrow accounting, the
|
|
5
|
+
* libp2p transport, the Ollama supervisor and the OpenAI-compatible bridge.
|
|
6
|
+
* The DeepSeek Harness adapters live in @genkaku/dsh-runtime and the two
|
|
7
|
+
* plugin packages above it.
|
|
8
|
+
*/
|
|
9
|
+
export * from "./bridge.js";
|
|
10
|
+
export * from "./chain.js";
|
|
11
|
+
export * from "./credit.js";
|
|
12
|
+
export * from "./dial.js";
|
|
13
|
+
export * from "./directory.js";
|
|
14
|
+
export * from "./discovery.js";
|
|
15
|
+
export * from "./hello.js";
|
|
16
|
+
export * from "./keys.js";
|
|
17
|
+
export * from "./llmBridge.js";
|
|
18
|
+
export * from "./lp.js";
|
|
19
|
+
export * from "./modeManager.js";
|
|
20
|
+
export * from "./netconfig.js";
|
|
21
|
+
export * from "./offer.js";
|
|
22
|
+
export * from "./ollama.js";
|
|
23
|
+
export * from "./p2pNode.js";
|
|
24
|
+
export * from "./paywall.js";
|
|
25
|
+
export * from "./publishOptIn.js";
|
|
26
|
+
export * from "./receipts.js";
|
|
27
|
+
export * from "./registry.js";
|
|
28
|
+
export * from "./rpc.js";
|
|
29
|
+
export * from "./status.js";
|
|
30
|
+
export * from "./userSession.js";
|
|
31
|
+
export * from "./workerSession.js";
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Hex } from 'viem';
|
|
2
|
+
export type PrivKey = Hex;
|
|
3
|
+
export type EthAddress = `0x${string}`;
|
|
4
|
+
/** safeStorage-shaped codec: electron main wires { encryptString, ... };
|
|
5
|
+
* tests/dev can pass a plaintext pass-through (files still skip keys repo). */
|
|
6
|
+
export type KeyCodec = {
|
|
7
|
+
encrypt(plain: string): Buffer;
|
|
8
|
+
decrypt(blob: Buffer): string;
|
|
9
|
+
};
|
|
10
|
+
export type KeyVault = {
|
|
11
|
+
/** the app's user identity key — generated on first use */
|
|
12
|
+
userKey(): PrivKey;
|
|
13
|
+
/** the app's worker identity key — generated on first use */
|
|
14
|
+
workerKey(): PrivKey;
|
|
15
|
+
/** derived addresses only (renderer-safe mirror of the keys) */
|
|
16
|
+
addresses(): {
|
|
17
|
+
user: EthAddress;
|
|
18
|
+
worker: EthAddress;
|
|
19
|
+
};
|
|
20
|
+
/** true when ANY key came from env overrides (dev-only telemetry) */
|
|
21
|
+
readonly usingEnvOverrides: boolean;
|
|
22
|
+
};
|
|
23
|
+
export declare function createKeyVault(opts: {
|
|
24
|
+
/** directory for the encrypted key file (app userData in main process) */
|
|
25
|
+
dir: string;
|
|
26
|
+
codec?: KeyCodec;
|
|
27
|
+
}): KeyVault;
|
|
28
|
+
/** Accepts 0x-prefixed or bare 32-byte hex; returns lowercase Hex or throws. */
|
|
29
|
+
export declare function normalizeKey(input: string): PrivKey;
|
|
30
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,MAAM,CAAA;AAG/B,MAAM,MAAM,OAAO,GAAG,GAAG,CAAA;AACzB,MAAM,MAAM,UAAU,GAAG,KAAK,MAAM,EAAE,CAAA;AAEtC;gFACgF;AAChF,MAAM,MAAM,QAAQ,GAAG;IACrB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IAC9B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAC9B,CAAA;AASD,MAAM,MAAM,QAAQ,GAAG;IACrB,2DAA2D;IAC3D,OAAO,IAAI,OAAO,CAAA;IAClB,6DAA6D;IAC7D,SAAS,IAAI,OAAO,CAAA;IACpB,gEAAgE;IAChE,SAAS,IAAI;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,MAAM,EAAE,UAAU,CAAA;KAAE,CAAA;IACrD,qEAAqE;IACrE,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAA;CACpC,CAAA;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACnC,0EAA0E;IAC1E,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,QAAQ,CAAA;CACjB,GAAG,QAAQ,CA2GX;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAQnD"}
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// GENKAKU: wallet key vault (P4.4.al) — Electron main process ONLY.
|
|
2
|
+
// Key material: one EVM keypair as the app's USER identity (receipt signing,
|
|
3
|
+
// escrow open + settle) and one as the WORKER identity (sidecar co-sign,
|
|
4
|
+
// claim). Encrypted by an injectable safeStorage-compatible codec and stored
|
|
5
|
+
// under the userData dir; dev/test overrides come from GENKAKU_USER_KEY /
|
|
6
|
+
// GENKAKU_WORKER_KEY env (documented DEV-ONLY).
|
|
7
|
+
//
|
|
8
|
+
// NO mainnet paths: consumers are testnet-locked at the chain layer (chain.ts
|
|
9
|
+
// refuses non-base-sepolia writes). Keys NEVER leave the main process — the
|
|
10
|
+
// renderer receives only the derived ADDRESSES.
|
|
11
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
14
|
+
const PLAINTEXT_CODEC = {
|
|
15
|
+
encrypt: plain => Buffer.from(plain, 'utf8'),
|
|
16
|
+
decrypt: blob => blob.toString('utf8'),
|
|
17
|
+
};
|
|
18
|
+
export function createKeyVault(opts) {
|
|
19
|
+
const codec = opts.codec ?? PLAINTEXT_CODEC;
|
|
20
|
+
const file = join(opts.dir, 'genkaku.keys.json');
|
|
21
|
+
mkdirSync(opts.dir, { recursive: true });
|
|
22
|
+
let vault = { version: 1 };
|
|
23
|
+
try {
|
|
24
|
+
const raw = JSON.parse(readFileSync(file, 'utf8'));
|
|
25
|
+
if (raw?.version === 1) {
|
|
26
|
+
vault = raw;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// first run / unreadable → fresh vault below
|
|
31
|
+
}
|
|
32
|
+
// DEV override (P4.4.al): env keys win when present. Deliberately loud.
|
|
33
|
+
const envUser = envKey('GENKAKU_USER_KEY');
|
|
34
|
+
const envWorker = envKey('GENKAKU_WORKER_KEY');
|
|
35
|
+
if (envUser != null || envWorker != null) {
|
|
36
|
+
console.warn('[genkaku:keys] DEV-ONLY env key override active '
|
|
37
|
+
+ `(user:${envUser != null ? 'set' : '-'} worker:${envWorker != null ? 'set' : '-'}) — never ship this path`);
|
|
38
|
+
}
|
|
39
|
+
let userKey = envUser;
|
|
40
|
+
let workerKey = envWorker;
|
|
41
|
+
const decode = (enc) => {
|
|
42
|
+
if (enc == null) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return normalizeKey(codec.decrypt(Buffer.from(enc, 'base64')));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error('[genkaku:keys] decrypt failed', error instanceof Error ? error.message : String(error));
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
userKey ??= decode(vault.userKeyEnc);
|
|
54
|
+
workerKey ??= decode(vault.workerKeyEnc);
|
|
55
|
+
const persist = () => {
|
|
56
|
+
let writeable = true;
|
|
57
|
+
try {
|
|
58
|
+
writeFileSync(file, JSON.stringify(vault), { mode: 0o600 });
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
writeable = false;
|
|
62
|
+
console.error('[genkaku:keys] vault write failed', error instanceof Error ? error.message : String(error));
|
|
63
|
+
}
|
|
64
|
+
if (!writeable) {
|
|
65
|
+
throw new Error('[genkaku:keys] cannot persist key vault');
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const lock = (name) => {
|
|
69
|
+
const current = userKey != null && name === 'user' ? userKey : workerKey != null && name === 'worker' ? workerKey : null;
|
|
70
|
+
if (current != null) {
|
|
71
|
+
return current;
|
|
72
|
+
}
|
|
73
|
+
const generated = normalizeKey(generatePrivateKey());
|
|
74
|
+
// keys from env overrides are intentionally NOT re-persisted — they stay
|
|
75
|
+
// env-scoped for the process lifetime.
|
|
76
|
+
if (name === 'user') {
|
|
77
|
+
userKey = generated;
|
|
78
|
+
if (envUser != null) {
|
|
79
|
+
return generated;
|
|
80
|
+
}
|
|
81
|
+
vault.userKeyEnc = codec.encrypt(generated).toString('base64');
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
workerKey = generated;
|
|
85
|
+
if (envWorker != null) {
|
|
86
|
+
return generated;
|
|
87
|
+
}
|
|
88
|
+
vault.workerKeyEnc = codec.encrypt(generated).toString('base64');
|
|
89
|
+
}
|
|
90
|
+
persist();
|
|
91
|
+
return generated;
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
userKey: () => lock('user'),
|
|
95
|
+
workerKey: () => lock('worker'),
|
|
96
|
+
addresses: () => ({
|
|
97
|
+
user: privateKeyToAccount(lock('user')).address,
|
|
98
|
+
worker: privateKeyToAccount(lock('worker')).address,
|
|
99
|
+
}),
|
|
100
|
+
get usingEnvOverrides() {
|
|
101
|
+
return envUser != null || envWorker != null;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Accepts 0x-prefixed or bare 32-byte hex; returns lowercase Hex or throws. */
|
|
106
|
+
export function normalizeKey(input) {
|
|
107
|
+
const bare = input.trim().toLowerCase().replace(/^0x/, '');
|
|
108
|
+
if (!/^[0-9a-f]{64}$/.test(bare)) {
|
|
109
|
+
throw new Error('[genkaku:keys] invalid private key material (expected 32-byte hex)');
|
|
110
|
+
}
|
|
111
|
+
return `0x${bare}`;
|
|
112
|
+
}
|
|
113
|
+
function envKey(name) {
|
|
114
|
+
const value = process.env[name];
|
|
115
|
+
if (value == null || value === '') {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
return normalizeKey(value);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
console.error(`[genkaku:keys] ${name} malformed`, error instanceof Error ? error.message : String(error));
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=keys.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type CompletionRequest = {
|
|
2
|
+
model: string;
|
|
3
|
+
prompt: string;
|
|
4
|
+
system?: string;
|
|
5
|
+
maxTokens?: number;
|
|
6
|
+
};
|
|
7
|
+
export type LocalCompletionSource = {
|
|
8
|
+
/** stream tokens; resolves when the source is exhausted */
|
|
9
|
+
stream(req: CompletionRequest, onDelta: (text: string) => void, signal?: AbortSignal): Promise<{
|
|
10
|
+
promptTokens: number;
|
|
11
|
+
completionTokens: number;
|
|
12
|
+
model: string;
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
export type ModelInfo = {
|
|
16
|
+
name: string;
|
|
17
|
+
sizeBytes?: number | undefined;
|
|
18
|
+
family?: string | undefined;
|
|
19
|
+
quantization?: string | undefined;
|
|
20
|
+
};
|
|
21
|
+
export type PullProgress = {
|
|
22
|
+
status: string;
|
|
23
|
+
percent?: number | undefined;
|
|
24
|
+
};
|
|
25
|
+
export declare function ollamaAdmin(host?: string): {
|
|
26
|
+
/** locally installed models (what the worker may choose to publish) */
|
|
27
|
+
list(): Promise<ModelInfo[]>;
|
|
28
|
+
/** download a model from the Ollama registry (registry name like "llama3:8b") */
|
|
29
|
+
pull(model: string, onProgress?: (p: PullProgress) => void, signal?: AbortSignal): Promise<{
|
|
30
|
+
status: string;
|
|
31
|
+
}>;
|
|
32
|
+
};
|
|
33
|
+
export declare function createOllamaSource(host?: string): LocalCompletionSource;
|
|
34
|
+
//# sourceMappingURL=llmBridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llmBridge.d.ts","sourceRoot":"","sources":["../src/llmBridge.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,2DAA2D;IAC3D,MAAM,CACJ,GAAG,EAAE,iBAAiB,EACtB,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAC/B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC9E,CAAA;AAOD,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC9B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAA;AAE3E,wBAAgB,WAAW,CAAC,IAAI,GAAE,MAAwD;IA6BtF,uEAAuE;YACzD,OAAO,CAAC,SAAS,EAAE,CAAC;IAmBlC,iFAAiF;gBAC/D,MAAM,eAAe,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,WAAW,WAAW,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;EAwBvH;AAKD,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,MAAwD,GAAG,qBAAqB,CA0ExH"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// GENKAKU: local LLM bridge (worker mode).
|
|
2
|
+
// Translates an in-flight libp2p worker stream into the machine's local Ollama
|
|
3
|
+
// API (127.0.0.1:11434 — same source the upstream agent uses via
|
|
4
|
+
// agent/agent_init_ollama.py), streaming tokens out and reporting metering.
|
|
5
|
+
//
|
|
6
|
+
// Deviation from P2 §6 note: the LLM-side transport is *local HTTP*, not the
|
|
7
|
+
// Python relay — the sidecar stays a thin, testable layer; a future variant can
|
|
8
|
+
// point it at a hermes gateway's internal completion API instead (the interface
|
|
9
|
+
// below is the seam: LocalCompletionSource).
|
|
10
|
+
export function ollamaAdmin(host = process.env.GENKAKU_OLLAMA_HOST ?? DEFAULT_HOST) {
|
|
11
|
+
async function readNdjson(res, onFrame) {
|
|
12
|
+
if (!res.ok || !res.body) {
|
|
13
|
+
throw new Error(`[genkaku:ollama] ${new URL(res.url).pathname} refused (${res.status})`);
|
|
14
|
+
}
|
|
15
|
+
const reader = res.body.getReader();
|
|
16
|
+
const decoder = new TextDecoder();
|
|
17
|
+
let buf = '';
|
|
18
|
+
while (true) {
|
|
19
|
+
const { value, done } = await reader.read();
|
|
20
|
+
if (done) {
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
buf += decoder.decode(value, { stream: true });
|
|
24
|
+
let nl;
|
|
25
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
26
|
+
const line = buf.slice(0, nl).trim();
|
|
27
|
+
buf = buf.slice(nl + 1);
|
|
28
|
+
if (!line) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
onFrame(JSON.parse(line));
|
|
33
|
+
}
|
|
34
|
+
catch { /* tolerate */ }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
/** locally installed models (what the worker may choose to publish) */
|
|
40
|
+
async list() {
|
|
41
|
+
const res = await fetch(new URL('/api/tags', host), { method: 'GET' });
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
throw new Error(`[genkaku:ollama] /api/tags refused (${res.status}) — is Ollama running?`);
|
|
44
|
+
}
|
|
45
|
+
const body = (await res.json());
|
|
46
|
+
return (body.models ?? []).map(m => ({
|
|
47
|
+
name: m.name,
|
|
48
|
+
sizeBytes: m.size,
|
|
49
|
+
family: m.details?.family,
|
|
50
|
+
quantization: m.details?.quantization_level,
|
|
51
|
+
}));
|
|
52
|
+
},
|
|
53
|
+
/** download a model from the Ollama registry (registry name like "llama3:8b") */
|
|
54
|
+
async pull(model, onProgress, signal) {
|
|
55
|
+
const res = await fetch(new URL('/api/pull', host), {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'content-type': 'application/json' },
|
|
58
|
+
body: JSON.stringify({ model, stream: true }),
|
|
59
|
+
...(signal === undefined ? {} : { signal }),
|
|
60
|
+
});
|
|
61
|
+
let status = 'unknown';
|
|
62
|
+
await readNdjson(res, (f) => {
|
|
63
|
+
const total = typeof f.total === 'number' ? f.total : undefined;
|
|
64
|
+
const completed = typeof f.completed === 'number' ? f.completed : undefined;
|
|
65
|
+
let percent;
|
|
66
|
+
if (total != null && completed != null && total > 0) {
|
|
67
|
+
percent = Math.min(100, Math.round((completed / total) * 100));
|
|
68
|
+
}
|
|
69
|
+
const frame = { status: String(f.status ?? ''), percent };
|
|
70
|
+
status = frame.status;
|
|
71
|
+
onProgress?.(frame);
|
|
72
|
+
});
|
|
73
|
+
return { status };
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const DEFAULT_HOST = 'http://127.0.0.1:11434';
|
|
78
|
+
export function createOllamaSource(host = process.env.GENKAKU_OLLAMA_HOST ?? DEFAULT_HOST) {
|
|
79
|
+
return {
|
|
80
|
+
async stream(req, onDelta, signal) {
|
|
81
|
+
const res = await fetch(new URL('/api/generate', host), {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'content-type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
model: req.model,
|
|
86
|
+
prompt: req.prompt,
|
|
87
|
+
system: req.system,
|
|
88
|
+
stream: true,
|
|
89
|
+
options: {
|
|
90
|
+
num_ctx: Number(process.env.GENKAKU_OLLAMA_NUM_CTX ?? 8192),
|
|
91
|
+
...(req.maxTokens ? { num_predict: req.maxTokens } : {}),
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
...(signal === undefined ? {} : { signal }),
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok || !res.body) {
|
|
97
|
+
throw new Error(`[genkaku:ollama] local source refused (${res.status}) — is Ollama running?`);
|
|
98
|
+
}
|
|
99
|
+
const reader = res.body.getReader();
|
|
100
|
+
const decoder = new TextDecoder();
|
|
101
|
+
let completionChars = 0;
|
|
102
|
+
let done = false;
|
|
103
|
+
let evalCount = 0;
|
|
104
|
+
let promptTokens = 0;
|
|
105
|
+
// LP-JSON over the ollama ndjson
|
|
106
|
+
let buf = '';
|
|
107
|
+
while (!done) {
|
|
108
|
+
const { value, done: rDone } = await reader.read();
|
|
109
|
+
if (rDone) {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
buf += decoder.decode(value, { stream: true });
|
|
113
|
+
let nl;
|
|
114
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
115
|
+
const line = buf.slice(0, nl).trim();
|
|
116
|
+
buf = buf.slice(nl + 1);
|
|
117
|
+
if (!line) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(line);
|
|
122
|
+
if (parsed.response) {
|
|
123
|
+
completionChars += parsed.response.length;
|
|
124
|
+
onDelta(parsed.response);
|
|
125
|
+
}
|
|
126
|
+
if (parsed.prompt_eval_count != null) {
|
|
127
|
+
promptTokens = parsed.prompt_eval_count;
|
|
128
|
+
}
|
|
129
|
+
if (parsed.eval_count != null) {
|
|
130
|
+
evalCount = parsed.eval_count;
|
|
131
|
+
}
|
|
132
|
+
if (parsed.done) {
|
|
133
|
+
done = true;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// tolerate partial frames (ollama does not truncate mid-line)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
promptTokens,
|
|
144
|
+
// prefer ollama's own count; fall back to the ~4-chars-per-token estimate
|
|
145
|
+
completionTokens: evalCount || Math.ceil(completionChars / 4),
|
|
146
|
+
model: req.model,
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=llmBridge.js.map
|
package/dist/lp.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare function lpEncode(obj: unknown): Uint8Array;
|
|
2
|
+
export type ByteChunk = Uint8Array | {
|
|
3
|
+
subarray(): Uint8Array;
|
|
4
|
+
};
|
|
5
|
+
/** Parse every COMPLETE frame in a byte blob (tests / whole-stream reads). */
|
|
6
|
+
export declare function lpFrames(chunks: ByteChunk[]): unknown[];
|
|
7
|
+
/**
|
|
8
|
+
* Incremental frame reader — feeds whatever bytes arrive and pops complete
|
|
9
|
+
* frames WITHOUT waiting for stream end.
|
|
10
|
+
*/
|
|
11
|
+
export declare class FrameReader {
|
|
12
|
+
private buf;
|
|
13
|
+
push(chunk: ByteChunk): void;
|
|
14
|
+
/** next complete frame, or undefined while the current one is still partial */
|
|
15
|
+
next(): unknown | undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* One durable write side per stream: `sink(queue.source())` once, then push
|
|
19
|
+
* frames as they happen; the write half stays open until .end().
|
|
20
|
+
*/
|
|
21
|
+
export declare class WriteQueue {
|
|
22
|
+
private items;
|
|
23
|
+
private waiters;
|
|
24
|
+
/** end() has been called — readers will see EOF after draining the items */
|
|
25
|
+
ended: boolean;
|
|
26
|
+
push(b: Uint8Array): void;
|
|
27
|
+
end(): void;
|
|
28
|
+
private drain;
|
|
29
|
+
source(): AsyncGenerator<Uint8Array>;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=lp.d.ts.map
|
package/dist/lp.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lp.d.ts","sourceRoot":"","sources":["../src/lp.ts"],"names":[],"mappings":"AAOA,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,CAQjD;AAED,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG;IAAE,QAAQ,IAAI,UAAU,CAAA;CAAE,CAAA;AA2B/D,8EAA8E;AAC9E,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,EAAE,CAgBvD;AAED;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,GAAG,CAAoB;IAE/B,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI;IAQ5B,+EAA+E;IAC/E,IAAI,IAAI,OAAO,GAAG,SAAS;CAQ5B;AAED;;;GAGG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,KAAK,CAAmB;IAChC,OAAO,CAAC,OAAO,CAAwB;IACvC,4EAA4E;IAC5E,KAAK,UAAQ;IAEb,IAAI,CAAC,CAAC,EAAE,UAAU,GAAG,IAAI;IAMzB,GAAG,IAAI,IAAI;IAKX,OAAO,CAAC,KAAK;IAIN,MAAM,IAAI,cAAc,CAAC,UAAU,CAAC;CAQ5C"}
|