-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathonsight.ts
More file actions
77 lines (70 loc) · 2.82 KB
/
Copy pathonsight.ts
File metadata and controls
77 lines (70 loc) · 2.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// Tiny client helper for the Onsight agent API.
//
// The one piece worth not reimplementing: the owner-action signature. Onsight
// gates money IN with x402, but every owner action afterwards (pick a winner,
// refund, edit the brief) is authorized by signing a canonical message with the
// same wallet. The message is action-, time-, AND body-bound, so a captured
// signature can't be replayed with a swapped body (e.g. a different photoId to
// redirect a payout). Get the format wrong and the server rejects you — or worse,
// you sign something you didn't mean to. This encodes it exactly once.
import { createHash } from "node:crypto";
/** Public base URL of the Onsight API. */
export const DEFAULT_BASE_URL = "https://onsight.photo";
/** Hash a request body exactly as the server does: sha256 of its JSON, hex. */
export function bodyHash(body?: unknown): string {
return createHash("sha256")
.update(JSON.stringify(body ?? {}))
.digest("hex");
}
/**
* The exact message an agent signs to authorize an owner action. Mirrors the
* server's WalletSigGuard byte-for-byte.
*
* @param method HTTP method, e.g. "POST"
* @param path full request path incl. query, as sent (the server's originalUrl)
* @param timestamp ms since epoch (Date.now()) — the value of X-Wallet-Timestamp
* @param body the JSON request body ({} / omit for none)
*/
export function ownerActionMessage(
method: string,
path: string,
timestamp: number,
body?: unknown,
): string {
return `Onsight agent: ${method.toUpperCase()} ${path} @ ${timestamp} body=${bodyHash(body)}`;
}
/** Minimal wallet — a viem LocalAccount (`privateKeyToAccount`) satisfies this. */
export interface WalletSigner {
address: string;
signMessage(args: { message: string }): Promise<`0x${string}`>;
}
/**
* Sign an owner action and return the `X-Wallet-*` headers to send with it.
* Pass the SAME body object you send in the request — the server rehashes the
* received body and rejects the call if it doesn't match what was signed.
*/
export async function signOwnerActionHeaders(
signer: WalletSigner,
method: string,
path: string,
body?: unknown,
timestamp: number = Date.now(),
): Promise<Record<string, string>> {
const message = ownerActionMessage(method, path, timestamp, body);
const signature = await signer.signMessage({ message });
return {
"X-Wallet-Address": signer.address,
"X-Wallet-Signature": signature,
"X-Wallet-Timestamp": String(timestamp),
};
}
/** URL to post + fund a mission — the x402-gated photo order. */
export function missionOrderUrl(
rewardUsd: number,
baseUrl: string = DEFAULT_BASE_URL,
): string {
if (!Number.isFinite(rewardUsd) || rewardUsd <= 0) {
throw new Error(`rewardUsd must be a positive number, got ${rewardUsd}`);
}
return `${baseUrl}/agent/missions?rewardUsd=${rewardUsd}`;
}