MVP demo: one operator-funded API key anyone can plug into an IDE/harness,
running real escrow-per-session marketplace conversations (llm.chat.v1)
behind the standard OpenAI /chat/completions wire shape.
- shared: ChatMessage gains tool role + tool_calls; normalizeChatMessage/
chatMessagesEquivalent keep supplier transcript and gateway mirror
hash-identical for receipts (field presence is part of the identity)
- supplier: /v1/chat/message accepts {messages,tools,tool_choice} alongside
legacy {content}; callOpenAiStream forwards tools, accumulates fragmented
delta.tool_calls, allows content-less tool turns; done frame carries the
assistant message verbatim + finish_reason + usage
- gateway: demo flag on api_keys (additive migration), create-demo-key CLI,
withdraw 403 + hidden deposit address for demo keys, per-IP demo rate
limit (demo keys skip the shared per-key limit), models filtered to chat
capability
- gateway executor (demoChat.ts): transcript-prefix affinity maps stateless
calls onto open sessions, forwards only unseen deltas, transparently
reopens on supplier idle-close; never falls back to one-shot (its settle
path assumes <=1 in-flight escrow per wallet)
- mutex scope: key mutex held only for chain work (open/close); off-chain
turns serialize on per-session locks so replicas serve users concurrently
- sweeper: chat-aware reclaim guard (35min vs 11min) and abandoned-session
cleanup; terminal state confirmed via on-chain queryUtxo (indexer keeps
lineage rows at their old state forever)
- deploy: kimi-chat replicas 2+3 compose files; chat idle timeout 2.5min
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
80 lines
2.6 KiB
TypeScript
80 lines
2.6 KiB
TypeScript
/**
|
|
* migration.test.ts — the demo-column migration on pre-existing databases.
|
|
*
|
|
* GatewayStore has no migration framework (pure CREATE TABLE IF NOT EXISTS),
|
|
* so opening a DB created before the `demo` column must ALTER it in place.
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import { tmpdir } from "os";
|
|
import { join } from "path";
|
|
import { mkdirSync } from "fs";
|
|
import { randomUUID } from "crypto";
|
|
import Database from "better-sqlite3";
|
|
import { GatewayStore } from "../src/db/store.js";
|
|
|
|
const LEGACY_API_KEYS_SQL = `
|
|
CREATE TABLE IF NOT EXISTS api_keys (
|
|
id TEXT PRIMARY KEY,
|
|
key_hash TEXT NOT NULL UNIQUE,
|
|
key_prefix TEXT NOT NULL,
|
|
label TEXT,
|
|
wallet_pkh TEXT NOT NULL,
|
|
deposit_address TEXT NOT NULL,
|
|
enc_priv_nonce TEXT NOT NULL,
|
|
enc_priv_ct TEXT NOT NULL,
|
|
enc_priv_tag TEXT NOT NULL,
|
|
master_key_version INTEGER NOT NULL DEFAULT 1,
|
|
created_at INTEGER NOT NULL,
|
|
disabled INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
`;
|
|
|
|
function keyRow(id: string) {
|
|
return {
|
|
id,
|
|
key_hash: `hash-${id}`,
|
|
key_prefix: "vk_test_xxxx",
|
|
label: null,
|
|
wallet_pkh: "pkh",
|
|
deposit_address: "addr_test1x",
|
|
enc_priv_nonce: "n",
|
|
enc_priv_ct: "c",
|
|
enc_priv_tag: "t",
|
|
master_key_version: 1,
|
|
created_at: Date.now(),
|
|
};
|
|
}
|
|
|
|
describe("GatewayStore demo-column migration", () => {
|
|
it("adds the demo column to a legacy DB and defaults existing keys to 0", () => {
|
|
const dir = join(tmpdir(), `gw-mig-${randomUUID()}`);
|
|
mkdirSync(dir, { recursive: true });
|
|
const raw = new Database(join(dir, "gateway.db"));
|
|
raw.exec(LEGACY_API_KEYS_SQL);
|
|
raw
|
|
.prepare(
|
|
`INSERT INTO api_keys (id, key_hash, key_prefix, label, wallet_pkh, deposit_address,
|
|
enc_priv_nonce, enc_priv_ct, enc_priv_tag, master_key_version, created_at, disabled)
|
|
VALUES (@id, @key_hash, @key_prefix, @label, @wallet_pkh, @deposit_address,
|
|
@enc_priv_nonce, @enc_priv_ct, @enc_priv_tag, @master_key_version, @created_at, 0)`,
|
|
)
|
|
.run(keyRow("legacy"));
|
|
raw.close();
|
|
|
|
const store = new GatewayStore(dir);
|
|
const legacy = store.getKeyByHash("hash-legacy");
|
|
expect(legacy?.demo).toBe(0);
|
|
|
|
store.insertKey({ ...keyRow("demo-key"), demo: 1 });
|
|
expect(store.getKeyByHash("hash-demo-key")?.demo).toBe(1);
|
|
});
|
|
|
|
it("persists the demo flag on fresh databases (default 0)", () => {
|
|
const dir = join(tmpdir(), `gw-fresh-${randomUUID()}`);
|
|
const store = new GatewayStore(dir);
|
|
store.insertKey(keyRow("plain"));
|
|
expect(store.getKeyByHash("hash-plain")?.demo).toBe(0);
|
|
});
|
|
});
|