@clawnet/template-minimal 0.0.1
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/.agents/skills/claude-agent-sdk/.claude-plugin/plugin.json +13 -0
- package/.agents/skills/claude-agent-sdk/SKILL.md +954 -0
- package/.agents/skills/claude-agent-sdk/references/mcp-servers-guide.md +387 -0
- package/.agents/skills/claude-agent-sdk/references/permissions-guide.md +429 -0
- package/.agents/skills/claude-agent-sdk/references/query-api-reference.md +437 -0
- package/.agents/skills/claude-agent-sdk/references/session-management.md +419 -0
- package/.agents/skills/claude-agent-sdk/references/subagents-patterns.md +464 -0
- package/.agents/skills/claude-agent-sdk/references/top-errors.md +503 -0
- package/.agents/skills/claude-agent-sdk/rules/claude-agent-sdk.md +96 -0
- package/.agents/skills/claude-agent-sdk/scripts/check-versions.sh +55 -0
- package/.agents/skills/claude-agent-sdk/templates/basic-query.ts +55 -0
- package/.agents/skills/claude-agent-sdk/templates/custom-mcp-server.ts +161 -0
- package/.agents/skills/claude-agent-sdk/templates/error-handling.ts +283 -0
- package/.agents/skills/claude-agent-sdk/templates/filesystem-settings.ts +211 -0
- package/.agents/skills/claude-agent-sdk/templates/multi-agent-workflow.ts +318 -0
- package/.agents/skills/claude-agent-sdk/templates/package.json +30 -0
- package/.agents/skills/claude-agent-sdk/templates/permission-control.ts +211 -0
- package/.agents/skills/claude-agent-sdk/templates/query-with-tools.ts +54 -0
- package/.agents/skills/claude-agent-sdk/templates/session-management.ts +151 -0
- package/.agents/skills/claude-agent-sdk/templates/subagents-orchestration.ts +166 -0
- package/.agents/skills/claude-agent-sdk/templates/tsconfig.json +22 -0
- package/.claude/settings.local.json +70 -0
- package/.claude/skills/moltbook-example/SKILL.md +79 -0
- package/.claude/skills/post/SKILL.md +130 -0
- package/.env.example +4 -0
- package/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/AGENTS.md +114 -0
- package/CLAUDE.md +532 -0
- package/README.md +44 -0
- package/api/index.ts +3 -0
- package/biome.json +14 -0
- package/clark_avatar.jpeg +0 -0
- package/package.json +21 -0
- package/scripts/wake.ts +38 -0
- package/skills/clawbook/HEARTBEAT.md +142 -0
- package/skills/clawbook/SKILL.md +219 -0
- package/skills/moltbook-example/SKILL.md +79 -0
- package/skills/moltbook-example/bot/index.ts +61 -0
- package/src/agent/prompts.ts +98 -0
- package/src/agent/runner.ts +526 -0
- package/src/agent/tool-definitions.ts +1151 -0
- package/src/agent-options.ts +14 -0
- package/src/bot-identity.ts +41 -0
- package/src/constants.ts +15 -0
- package/src/handlers/heartbeat.ts +21 -0
- package/src/handlers/openai-compat.ts +95 -0
- package/src/handlers/post.ts +21 -0
- package/src/identity.ts +83 -0
- package/src/index.ts +30 -0
- package/src/middleware/cron-auth.ts +53 -0
- package/src/middleware/sigma-auth.ts +147 -0
- package/src/runs.ts +49 -0
- package/tests/agent/prompts.test.ts +172 -0
- package/tests/agent/runner.test.ts +353 -0
- package/tests/agent/tool-definitions.test.ts +171 -0
- package/tests/constants.test.ts +24 -0
- package/tests/handlers/openai-compat.test.ts +128 -0
- package/tests/handlers.test.ts +133 -0
- package/tests/identity.test.ts +66 -0
- package/tests/index.test.ts +108 -0
- package/tests/middleware/cron-auth.test.ts +99 -0
- package/tests/middleware/sigma-auth.test.ts +198 -0
- package/tests/runs.test.ts +56 -0
- package/tests/skill.test.ts +71 -0
- package/tsconfig.json +14 -0
- package/vercel.json +9 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RunAgentOptions } from "./agent/runner";
|
|
2
|
+
import { getBotIdentity } from "./bot-identity";
|
|
3
|
+
import { CLAWBOOK_API_URL } from "./constants";
|
|
4
|
+
|
|
5
|
+
export async function agentOptions(): Promise<RunAgentOptions> {
|
|
6
|
+
return {
|
|
7
|
+
clawbookApiUrl: CLAWBOOK_API_URL,
|
|
8
|
+
sigmaMemberWif: process.env.SIGMA_MEMBER_WIF ?? "",
|
|
9
|
+
moltbookApiKey: process.env.MOLTBOOK_API_KEY,
|
|
10
|
+
anthropicAuthToken:
|
|
11
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN ?? process.env.ANTHROPIC_AUTH_TOKEN,
|
|
12
|
+
botIdentity: await getBotIdentity(),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CLAWBOOK_API_URL } from "./constants";
|
|
2
|
+
import { createIdentity, getIdKey } from "./identity";
|
|
3
|
+
|
|
4
|
+
let _cached: { idKey: string; name: string } | undefined | null = null;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Derive bot identity from SIGMA_MEMBER_WIF.
|
|
8
|
+
* Fetches display name from on-chain BAP identity via profile API.
|
|
9
|
+
* Cached after first successful call. Returns undefined if WIF is missing or invalid.
|
|
10
|
+
*/
|
|
11
|
+
export async function getBotIdentity(): Promise<
|
|
12
|
+
{ idKey: string; name: string } | undefined
|
|
13
|
+
> {
|
|
14
|
+
if (_cached !== null) return _cached;
|
|
15
|
+
const wif = process.env.SIGMA_MEMBER_WIF;
|
|
16
|
+
if (!wif) {
|
|
17
|
+
_cached = undefined;
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const identity = createIdentity(wif);
|
|
22
|
+
const idKey = getIdKey(identity);
|
|
23
|
+
let name = idKey.slice(0, 8);
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(
|
|
26
|
+
`${CLAWBOOK_API_URL}/api/profiles/${encodeURIComponent(idKey)}`,
|
|
27
|
+
);
|
|
28
|
+
const data = await res.json();
|
|
29
|
+
if (data?.data?.identity?.alternateName) {
|
|
30
|
+
name = data.data.identity.alternateName;
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
/* use truncated idKey as fallback */
|
|
34
|
+
}
|
|
35
|
+
_cached = { idKey, name };
|
|
36
|
+
return _cached;
|
|
37
|
+
} catch {
|
|
38
|
+
_cached = undefined;
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const APP_NAME = "clawbook";
|
|
2
|
+
export const BAP_PREFIX = "1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT";
|
|
3
|
+
export const B_PREFIX = "19HxigV4QyBv3tHpQVcUEQyq1pzZVdoAut";
|
|
4
|
+
export const MAP_PREFIX = "1PuQa7K62MiKCtssSLKy1kh56WWU7MtUR5";
|
|
5
|
+
export const AIP_PREFIX = "15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva";
|
|
6
|
+
export const BMAP_URL = "https://bmap-api-production.up.railway.app";
|
|
7
|
+
export const SIGMA_API_URL = "https://api.sigmaidentity.com/v1";
|
|
8
|
+
export const SIGMA_AUTH_URL = "https://auth.sigmaidentity.com";
|
|
9
|
+
export const ORDFS_URL = "https://ordfs.network";
|
|
10
|
+
export const BSOCIAL_API_URL =
|
|
11
|
+
"https://bsocial-overlay-production.up.railway.app/api/v1";
|
|
12
|
+
export const CLAWBOOK_API_URL =
|
|
13
|
+
process.env.CLAWBOOK_API_URL || "https://clawbook.network";
|
|
14
|
+
export const MOLTBOOK_API_URL = "https://www.moltbook.com/api/v1";
|
|
15
|
+
export const SITE_DESCRIPTION = "The front page of the agent blockchain";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AgentTurnResult } from "../agent/runner";
|
|
2
|
+
import { runAgentTurn } from "../agent/runner";
|
|
3
|
+
import { getBotIdentity } from "../bot-identity";
|
|
4
|
+
import { CLAWBOOK_API_URL } from "../constants";
|
|
5
|
+
|
|
6
|
+
export async function handleHeartbeat(): Promise<AgentTurnResult> {
|
|
7
|
+
try {
|
|
8
|
+
return await runAgentTurn("heartbeat", {
|
|
9
|
+
clawbookApiUrl: CLAWBOOK_API_URL,
|
|
10
|
+
sigmaMemberWif: process.env.SIGMA_MEMBER_WIF ?? "",
|
|
11
|
+
moltbookApiKey: process.env.MOLTBOOK_API_KEY,
|
|
12
|
+
anthropicAuthToken:
|
|
13
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN ?? process.env.ANTHROPIC_AUTH_TOKEN,
|
|
14
|
+
botIdentity: await getBotIdentity(),
|
|
15
|
+
});
|
|
16
|
+
} catch (err) {
|
|
17
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
18
|
+
console.error(`[heartbeat] error: ${msg}`);
|
|
19
|
+
return { success: false, summary: "", actions: [], error: msg };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { stream } from "hono/streaming";
|
|
3
|
+
import { runAgentTurn } from "../agent/runner";
|
|
4
|
+
import { agentOptions } from "../agent-options";
|
|
5
|
+
|
|
6
|
+
interface ChatMessage {
|
|
7
|
+
role: string;
|
|
8
|
+
content: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ChatCompletionRequest {
|
|
12
|
+
messages?: ChatMessage[];
|
|
13
|
+
stream?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function extractLastUserMessage(messages: ChatMessage[]): string | null {
|
|
17
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
18
|
+
if (messages[i].role === "user" && messages[i].content) {
|
|
19
|
+
return messages[i].content;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function handleChatCompletions(c: Context) {
|
|
26
|
+
const body = await c.req.json<ChatCompletionRequest>();
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
!body.messages ||
|
|
30
|
+
!Array.isArray(body.messages) ||
|
|
31
|
+
body.messages.length === 0
|
|
32
|
+
) {
|
|
33
|
+
return c.json(
|
|
34
|
+
{ error: "messages array is required and must not be empty" },
|
|
35
|
+
400,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const userMessage = extractLastUserMessage(body.messages);
|
|
40
|
+
if (!userMessage) {
|
|
41
|
+
return c.json({ error: "No user message found in messages array" }, 400);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const opts = await agentOptions();
|
|
45
|
+
const result = await runAgentTurn("conversation", {
|
|
46
|
+
...opts,
|
|
47
|
+
customMessage: userMessage,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const runId = `run_${crypto.randomUUID()}`;
|
|
51
|
+
const created = Math.floor(Date.now() / 1000);
|
|
52
|
+
const content = result.success
|
|
53
|
+
? result.summary || "Agent completed with no summary."
|
|
54
|
+
: `Error: ${result.error || "Agent failed"}`;
|
|
55
|
+
|
|
56
|
+
if (body.stream) {
|
|
57
|
+
return stream(c, async (s) => {
|
|
58
|
+
c.header("Content-Type", "text/event-stream");
|
|
59
|
+
c.header("Cache-Control", "no-cache");
|
|
60
|
+
c.header("Connection", "keep-alive");
|
|
61
|
+
|
|
62
|
+
const chunk = JSON.stringify({
|
|
63
|
+
id: runId,
|
|
64
|
+
object: "chat.completion.chunk",
|
|
65
|
+
created,
|
|
66
|
+
model: "clarkling",
|
|
67
|
+
choices: [
|
|
68
|
+
{
|
|
69
|
+
index: 0,
|
|
70
|
+
delta: { role: "assistant", content },
|
|
71
|
+
finish_reason: "stop",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await s.write(`data: ${chunk}\n\n`);
|
|
77
|
+
await s.write("data: [DONE]\n\n");
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return c.json({
|
|
82
|
+
id: runId,
|
|
83
|
+
object: "chat.completion",
|
|
84
|
+
created,
|
|
85
|
+
model: "clarkling",
|
|
86
|
+
choices: [
|
|
87
|
+
{
|
|
88
|
+
index: 0,
|
|
89
|
+
message: { role: "assistant", content },
|
|
90
|
+
finish_reason: "stop",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AgentTurnResult } from "../agent/runner";
|
|
2
|
+
import { runAgentTurn } from "../agent/runner";
|
|
3
|
+
import { getBotIdentity } from "../bot-identity";
|
|
4
|
+
import { CLAWBOOK_API_URL } from "../constants";
|
|
5
|
+
|
|
6
|
+
export async function handlePost(): Promise<AgentTurnResult> {
|
|
7
|
+
try {
|
|
8
|
+
return await runAgentTurn("scheduled_post", {
|
|
9
|
+
clawbookApiUrl: CLAWBOOK_API_URL,
|
|
10
|
+
sigmaMemberWif: process.env.SIGMA_MEMBER_WIF ?? "",
|
|
11
|
+
moltbookApiKey: process.env.MOLTBOOK_API_KEY,
|
|
12
|
+
anthropicAuthToken:
|
|
13
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN ?? process.env.ANTHROPIC_AUTH_TOKEN,
|
|
14
|
+
botIdentity: await getBotIdentity(),
|
|
15
|
+
});
|
|
16
|
+
} catch (err) {
|
|
17
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
18
|
+
console.error(`[post] error: ${msg}`);
|
|
19
|
+
return { success: false, summary: "", actions: [], error: msg };
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/identity.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { BAP, type MasterID } from "bsv-bap";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BotIdentity wrapper around BAP MasterID
|
|
5
|
+
* Provides simplified interface for clawbook-bot
|
|
6
|
+
*/
|
|
7
|
+
export interface BotIdentity {
|
|
8
|
+
bap: BAP;
|
|
9
|
+
masterId: MasterID;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a BAP identity from a WIF key
|
|
14
|
+
* Uses Type 42 format (recommended over deprecated BIP32)
|
|
15
|
+
*
|
|
16
|
+
* @param wif - Wallet Import Format private key
|
|
17
|
+
* @returns BotIdentity object containing BAP instance and MasterID
|
|
18
|
+
*/
|
|
19
|
+
export function createIdentity(wif: string): BotIdentity {
|
|
20
|
+
const bap = new BAP({ rootPk: wif });
|
|
21
|
+
|
|
22
|
+
// Create a new identity
|
|
23
|
+
// For a bot, we'll use a simple name like "clawbook-bot"
|
|
24
|
+
const masterId = bap.newId("clawbook-bot");
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
bap,
|
|
28
|
+
masterId,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the BAP identity key (idKey) for this identity
|
|
34
|
+
*
|
|
35
|
+
* @param identity - BotIdentity object
|
|
36
|
+
* @returns BAP identity key string
|
|
37
|
+
*/
|
|
38
|
+
export function getIdKey(identity: BotIdentity): string {
|
|
39
|
+
return identity.masterId.getIdentityKey();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sign OP_RETURN data with AIP (Author Identity Protocol)
|
|
44
|
+
*
|
|
45
|
+
* Takes hex-encoded data arrays and returns signed data including AIP signature.
|
|
46
|
+
* The returned array includes the original data plus AIP protocol elements:
|
|
47
|
+
* [AIP_PREFIX, "BITCOIN_ECDSA", address, signature, indices]
|
|
48
|
+
*
|
|
49
|
+
* @param identity - BotIdentity object
|
|
50
|
+
* @param opReturnData - Array of number arrays (hex-encoded data)
|
|
51
|
+
* @returns Signed OP_RETURN data with AIP signature appended
|
|
52
|
+
*/
|
|
53
|
+
export function signOpReturn(
|
|
54
|
+
identity: BotIdentity,
|
|
55
|
+
opReturnData: number[][],
|
|
56
|
+
): number[][] {
|
|
57
|
+
return identity.masterId.signOpReturnWithAIP(opReturnData);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get the current signing address for this identity
|
|
62
|
+
*
|
|
63
|
+
* @param identity - BotIdentity object
|
|
64
|
+
* @returns Bitcoin address (base58check)
|
|
65
|
+
*/
|
|
66
|
+
export function getCurrentAddress(identity: BotIdentity): string {
|
|
67
|
+
return identity.masterId.getCurrentAddress();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Sign a message with this identity
|
|
72
|
+
* Returns signature and address for verification
|
|
73
|
+
*
|
|
74
|
+
* @param identity - BotIdentity object
|
|
75
|
+
* @param message - Message as number array (UTF-8 bytes or hex)
|
|
76
|
+
* @returns Object containing address and signature
|
|
77
|
+
*/
|
|
78
|
+
export function signMessage(
|
|
79
|
+
identity: BotIdentity,
|
|
80
|
+
message: number[],
|
|
81
|
+
): { address: string; signature: string } {
|
|
82
|
+
return identity.masterId.signMessage(message);
|
|
83
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
|
|
3
|
+
const app = new Hono();
|
|
4
|
+
|
|
5
|
+
app.get("/", (c) => {
|
|
6
|
+
return c.json({
|
|
7
|
+
name: "clawnet-minimal",
|
|
8
|
+
version: "0.1.0",
|
|
9
|
+
status: "ok",
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
app.post("/api/agent", async (c) => {
|
|
14
|
+
const body = await c.req.json();
|
|
15
|
+
console.log("Agent request:", body);
|
|
16
|
+
|
|
17
|
+
return c.json({
|
|
18
|
+
success: true,
|
|
19
|
+
message: "Agent endpoint ready",
|
|
20
|
+
timestamp: new Date().toISOString(),
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
|
|
25
|
+
console.log(`Server starting on port ${port}`);
|
|
26
|
+
|
|
27
|
+
export default {
|
|
28
|
+
port,
|
|
29
|
+
fetch: app.fetch,
|
|
30
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import type { MiddlewareHandler } from "hono";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Middleware to authenticate cron endpoints using CRON_SECRET bearer token.
|
|
6
|
+
*
|
|
7
|
+
* If CRON_SECRET is not set (dev mode), logs a warning and allows through.
|
|
8
|
+
* Otherwise, requires Authorization: Bearer <CRON_SECRET> header with timing-safe comparison.
|
|
9
|
+
*/
|
|
10
|
+
export const cronAuth: MiddlewareHandler = async (c, next) => {
|
|
11
|
+
const secret = process.env.CRON_SECRET;
|
|
12
|
+
|
|
13
|
+
// Dev mode: no secret set
|
|
14
|
+
if (!secret) {
|
|
15
|
+
console.warn(
|
|
16
|
+
"[cronAuth] CRON_SECRET not set - allowing request (dev mode)",
|
|
17
|
+
);
|
|
18
|
+
return next();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Extract bearer token
|
|
22
|
+
const authHeader = c.req.header("Authorization");
|
|
23
|
+
if (!authHeader) {
|
|
24
|
+
return c.json({ error: "Missing Authorization header" }, 401);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const [scheme, token] = authHeader.split(" ");
|
|
28
|
+
if (scheme !== "Bearer" || !token) {
|
|
29
|
+
return c.json({ error: "Invalid Authorization header format" }, 401);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Timing-safe comparison
|
|
33
|
+
// timingSafeEqual requires equal-length buffers, so check lengths first
|
|
34
|
+
if (token.length !== secret.length) {
|
|
35
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const encoder = new TextEncoder();
|
|
39
|
+
const tokenBuf = encoder.encode(token);
|
|
40
|
+
const secretBuf = encoder.encode(secret);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
if (!timingSafeEqual(tokenBuf, secretBuf)) {
|
|
44
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// timingSafeEqual can throw if buffers are different lengths
|
|
48
|
+
// (though we pre-check, this is defensive)
|
|
49
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return next();
|
|
53
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { PrivateKey } from "@bsv/sdk";
|
|
2
|
+
import { parseAuthToken, verifyAuthToken } from "bitcoin-auth";
|
|
3
|
+
import type { MiddlewareHandler } from "hono";
|
|
4
|
+
|
|
5
|
+
// Cache the owner public key to avoid repeated derivation
|
|
6
|
+
let cachedOwnerPubkey: string | null = null;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Derives the owner's public key from SIGMA_MEMBER_WIF env var.
|
|
10
|
+
* Result is cached after first derivation.
|
|
11
|
+
*/
|
|
12
|
+
export function getOwnerPubkey(): string {
|
|
13
|
+
if (cachedOwnerPubkey) {
|
|
14
|
+
return cachedOwnerPubkey;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const wif = process.env.SIGMA_MEMBER_WIF;
|
|
18
|
+
if (!wif) {
|
|
19
|
+
throw new Error("SIGMA_MEMBER_WIF environment variable not set");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const privateKey = PrivateKey.fromWif(wif);
|
|
24
|
+
cachedOwnerPubkey = privateKey.toPublicKey().toString();
|
|
25
|
+
return cachedOwnerPubkey;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Failed to derive public key from SIGMA_MEMBER_WIF: ${error}`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Hono middleware that verifies Bitcoin Signed Message authentication tokens.
|
|
35
|
+
*
|
|
36
|
+
* Extracts and verifies the Authorization: Bearer <token> header:
|
|
37
|
+
* 1. Parses the token using bitcoin-auth parseAuthToken()
|
|
38
|
+
* 2. Verifies the signature using bitcoin-auth verifyAuthToken()
|
|
39
|
+
* 3. Compares the pubkey against the owner's public key (derived from SIGMA_MEMBER_WIF)
|
|
40
|
+
*
|
|
41
|
+
* Returns:
|
|
42
|
+
* - 401 if token is missing, malformed, invalid signature, or expired
|
|
43
|
+
* - 403 if token is valid but from wrong identity (pubkey mismatch)
|
|
44
|
+
* - Calls next() if valid and matches owner identity
|
|
45
|
+
*/
|
|
46
|
+
export const sigmaAuth: MiddlewareHandler = async (c, next) => {
|
|
47
|
+
const path = new URL(c.req.url).pathname;
|
|
48
|
+
console.log(`[sigma-auth] ${path} start method=${c.req.method}`);
|
|
49
|
+
|
|
50
|
+
// Extract Authorization header
|
|
51
|
+
const authHeader = c.req.header("Authorization");
|
|
52
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
53
|
+
return c.json({ error: "Missing or invalid Authorization header" }, 401);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const token = authHeader.substring(7); // Remove "Bearer " prefix
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Parse token
|
|
60
|
+
const parsed = parseAuthToken(token);
|
|
61
|
+
if (!parsed) {
|
|
62
|
+
console.log(`[sigma-auth] ${path} REJECT: invalid token format`);
|
|
63
|
+
return c.json({ error: "Invalid token format" }, 401);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(
|
|
67
|
+
`[sigma-auth] ${path} parsed token OK, pubkey=${parsed.pubkey.slice(0, 12)}...`,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const requestPath = path;
|
|
71
|
+
|
|
72
|
+
// Read body if present (for POST/PUT/PATCH requests)
|
|
73
|
+
let bodyText: string | undefined;
|
|
74
|
+
if (
|
|
75
|
+
c.req.method === "POST" ||
|
|
76
|
+
c.req.method === "PUT" ||
|
|
77
|
+
c.req.method === "PATCH"
|
|
78
|
+
) {
|
|
79
|
+
try {
|
|
80
|
+
bodyText = await c.req.text();
|
|
81
|
+
} catch {
|
|
82
|
+
bodyText = undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(
|
|
87
|
+
`[sigma-auth] ${path} body=${bodyText ? `${bodyText.length}b` : "none"}`,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Check token expiry (5 minute window)
|
|
91
|
+
const tokenTimestamp = new Date(parsed.timestamp);
|
|
92
|
+
const now = new Date();
|
|
93
|
+
const maxAgeMs = 5 * 60 * 1000; // 5 minutes
|
|
94
|
+
if (now.getTime() - tokenTimestamp.getTime() > maxAgeMs) {
|
|
95
|
+
console.log(
|
|
96
|
+
`[sigma-auth] ${requestPath} REJECT: expired (age=${Math.round((now.getTime() - tokenTimestamp.getTime()) / 1000)}s)`,
|
|
97
|
+
);
|
|
98
|
+
return c.json({ error: "Invalid or expired token" }, 401);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const authPayload = {
|
|
102
|
+
requestPath,
|
|
103
|
+
timestamp: parsed.timestamp,
|
|
104
|
+
body: bodyText,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Verify token signature
|
|
108
|
+
console.log(`[sigma-auth] ${path} verifying signature...`);
|
|
109
|
+
const isValid = verifyAuthToken(token, authPayload);
|
|
110
|
+
if (!isValid) {
|
|
111
|
+
console.log(
|
|
112
|
+
`[sigma-auth] ${requestPath} REJECT: signature verification failed`,
|
|
113
|
+
);
|
|
114
|
+
return c.json({ error: "Invalid or expired token" }, 401);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Verify identity matches owner
|
|
118
|
+
let ownerPubkey: string;
|
|
119
|
+
try {
|
|
120
|
+
ownerPubkey = getOwnerPubkey();
|
|
121
|
+
} catch {
|
|
122
|
+
return c.json(
|
|
123
|
+
{
|
|
124
|
+
error: "Server configuration error: Unable to verify identity",
|
|
125
|
+
},
|
|
126
|
+
500,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (parsed.pubkey !== ownerPubkey) {
|
|
131
|
+
console.log(
|
|
132
|
+
`[sigma-auth] ${requestPath} REJECT: pubkey mismatch (got=${parsed.pubkey.slice(0, 12)}... want=${ownerPubkey.slice(0, 12)}...)`,
|
|
133
|
+
);
|
|
134
|
+
return c.json({ error: "Forbidden: Identity mismatch" }, 403);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(
|
|
138
|
+
`[sigma-auth] ${requestPath} OK (pubkey=${parsed.pubkey.slice(0, 12)}...)`,
|
|
139
|
+
);
|
|
140
|
+
await next();
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.error(
|
|
143
|
+
`[sigma-auth] ${path} CRASH: ${err instanceof Error ? err.message : String(err)}`,
|
|
144
|
+
);
|
|
145
|
+
return c.json({ error: "Authentication processing error" }, 500);
|
|
146
|
+
}
|
|
147
|
+
};
|
package/src/runs.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { TriggerType } from "./agent/prompts";
|
|
2
|
+
import type { AgentTurnResult } from "./agent/runner";
|
|
3
|
+
|
|
4
|
+
export interface RunState {
|
|
5
|
+
id: string;
|
|
6
|
+
status: "pending" | "running" | "completed" | "failed";
|
|
7
|
+
trigger: TriggerType;
|
|
8
|
+
result?: AgentTurnResult;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
completedAt?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const runs = new Map<string, RunState>();
|
|
14
|
+
|
|
15
|
+
const ONE_HOUR_MS = 60 * 60 * 1000;
|
|
16
|
+
|
|
17
|
+
function pruneOldRuns(): void {
|
|
18
|
+
const cutoff = Date.now() - ONE_HOUR_MS;
|
|
19
|
+
for (const [id, run] of runs) {
|
|
20
|
+
if (new Date(run.createdAt).getTime() < cutoff) {
|
|
21
|
+
runs.delete(id);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createRun(trigger: TriggerType): RunState {
|
|
27
|
+
pruneOldRuns();
|
|
28
|
+
const run: RunState = {
|
|
29
|
+
id: crypto.randomUUID(),
|
|
30
|
+
status: "pending",
|
|
31
|
+
trigger,
|
|
32
|
+
createdAt: new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
runs.set(run.id, run);
|
|
35
|
+
return run;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function updateRun(id: string, update: Partial<RunState>): void {
|
|
39
|
+
const run = runs.get(id);
|
|
40
|
+
if (!run) return;
|
|
41
|
+
Object.assign(run, update);
|
|
42
|
+
if (update.status === "completed" || update.status === "failed") {
|
|
43
|
+
run.completedAt = new Date().toISOString();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getRun(id: string): RunState | undefined {
|
|
48
|
+
return runs.get(id);
|
|
49
|
+
}
|