@livo-build/runtime 0.2.12 → 0.2.13

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.
@@ -0,0 +1,74 @@
1
+ interface MinimalEnv {
2
+ [key: string]: unknown;
3
+ }
4
+ export interface EmailOptions {
5
+ /** Platform email API base, e.g. https://<site>/email. Default env EMAIL_API_URL. */
6
+ apiUrl?: string;
7
+ /** Bearer scoping calls to this project. Default env EMAIL_API_TOKEN. */
8
+ apiToken?: string;
9
+ }
10
+ export type EmailDirection = "in" | "out";
11
+ /** One message in the project's inbox/outbox (newest first from `list`). */
12
+ export interface EmailMessage {
13
+ id: string;
14
+ direction: EmailDirection;
15
+ from: string;
16
+ to: string[];
17
+ cc: string[];
18
+ subject: string;
19
+ text: string | null;
20
+ html: string | null;
21
+ messageId: string | null;
22
+ inReplyTo: string | null;
23
+ read: boolean;
24
+ receivedAt: number;
25
+ }
26
+ export interface ListEmailsOptions {
27
+ /** Only unread inbound/outbound messages. */
28
+ unread?: boolean;
29
+ /** Max messages to return (1–100, default 20). */
30
+ limit?: number;
31
+ }
32
+ export interface SendEmailOptions {
33
+ to: string | string[];
34
+ subject: string;
35
+ text?: string;
36
+ html?: string;
37
+ /** Display name for the From (address is always the project's own). */
38
+ fromName?: string;
39
+ /** Message-ID this is replying to (sets In-Reply-To/References for threading). */
40
+ inReplyTo?: string;
41
+ }
42
+ export interface SendEmailResult {
43
+ ok: boolean;
44
+ message_id: string | null;
45
+ }
46
+ export declare class Email {
47
+ private readonly apiUrl?;
48
+ private readonly apiToken?;
49
+ constructor(env: MinimalEnv | undefined, options?: EmailOptions);
50
+ /** Recent messages, newest first. Defaults to the whole inbox+outbox (20). */
51
+ list(opts?: ListEmailsOptions): Promise<EmailMessage[]>;
52
+ /** Unread inbound messages, newest first (shorthand for list({ unread: true })). */
53
+ unread(limit?: number): Promise<EmailMessage[]>;
54
+ /** Fetch one message by id (full body). Returns null if it isn't in this inbox. */
55
+ get(id: string): Promise<EmailMessage | null>;
56
+ /** Mark a message read (or unread with read=false). */
57
+ markRead(id: string, read?: boolean): Promise<void>;
58
+ /** Send a new message AS the project ({slug}@livo.build). */
59
+ send(opts: SendEmailOptions): Promise<SendEmailResult>;
60
+ /**
61
+ * Reply to an inbound message: sends back to its sender, threads via Message-ID,
62
+ * prefixes "Re:" if needed, and marks the original read. Pass `to` to override
63
+ * the recipient.
64
+ */
65
+ reply(msg: EmailMessage, opts: {
66
+ text?: string;
67
+ html?: string;
68
+ fromName?: string;
69
+ to?: string | string[];
70
+ }): Promise<SendEmailResult>;
71
+ /** Internal: call an email API sub-path with the project bearer; throws on error. */
72
+ private call;
73
+ }
74
+ export {};
package/dist/email.js ADDED
@@ -0,0 +1,100 @@
1
+ // Email — the project's own inbox at {slug}@livo.build. Every Livo project gets a
2
+ // canonical address; this helper lets a deployed bot/server/keeper read incoming
3
+ // mail and send (or reply) AS the project, with zero key/SMTP handling. All calls
4
+ // go to the platform (EMAIL_API_URL, authorized by EMAIL_API_TOKEN, both injected
5
+ // at deploy); inbound is parsed + stored by the platform Email Worker, and sends
6
+ // go out through the platform's transactional provider — so a leaked Worker can
7
+ // only send AS this project, never spoof another.
8
+ //
9
+ // const email = new Email(env);
10
+ // const inbox = await email.list({ unread: true }); // newest first
11
+ // for (const m of inbox) {
12
+ // const full = await email.get(m.id); // full body
13
+ // await email.reply(full, { text: "thanks!" }); // sends + marks read
14
+ // }
15
+ // await email.send({ to: "a@b.com", subject: "hi", text: "hello" });
16
+ //
17
+ // The From address is always the project's own ({slug}@livo.build) — pass
18
+ // `fromName` for a display name. To poll on a schedule, call list() from a keeper;
19
+ // to react instantly, point a webhook at your api/ Worker (see livo://skill/email).
20
+ export class Email {
21
+ apiUrl;
22
+ apiToken;
23
+ constructor(env, options = {}) {
24
+ this.apiUrl = (options.apiUrl ?? env?.EMAIL_API_URL)?.replace(/\/$/, "");
25
+ this.apiToken = options.apiToken ?? env?.EMAIL_API_TOKEN;
26
+ }
27
+ /** Recent messages, newest first. Defaults to the whole inbox+outbox (20). */
28
+ async list(opts = {}) {
29
+ const qs = new URLSearchParams();
30
+ if (opts.unread)
31
+ qs.set("unread", "1");
32
+ if (opts.limit)
33
+ qs.set("limit", String(opts.limit));
34
+ const r = await this.call("GET", `list${qs.toString() ? `?${qs}` : ""}`);
35
+ return (r.messages ?? []);
36
+ }
37
+ /** Unread inbound messages, newest first (shorthand for list({ unread: true })). */
38
+ async unread(limit) {
39
+ return this.list({ unread: true, limit });
40
+ }
41
+ /** Fetch one message by id (full body). Returns null if it isn't in this inbox. */
42
+ async get(id) {
43
+ const r = await this.call("POST", "get", { id });
44
+ return r.message ?? null;
45
+ }
46
+ /** Mark a message read (or unread with read=false). */
47
+ async markRead(id, read = true) {
48
+ await this.call("POST", "mark-read", { id, read });
49
+ }
50
+ /** Send a new message AS the project ({slug}@livo.build). */
51
+ async send(opts) {
52
+ const r = await this.call("POST", "send", {
53
+ to: Array.isArray(opts.to) ? opts.to : [opts.to],
54
+ subject: opts.subject,
55
+ text: opts.text,
56
+ html: opts.html,
57
+ fromName: opts.fromName,
58
+ inReplyTo: opts.inReplyTo,
59
+ });
60
+ return { ok: r.ok === true, message_id: r.message_id ?? null };
61
+ }
62
+ /**
63
+ * Reply to an inbound message: sends back to its sender, threads via Message-ID,
64
+ * prefixes "Re:" if needed, and marks the original read. Pass `to` to override
65
+ * the recipient.
66
+ */
67
+ async reply(msg, opts) {
68
+ const subject = /^re:/i.test(msg.subject) ? msg.subject : `Re: ${msg.subject}`;
69
+ const result = await this.send({
70
+ to: opts.to ?? msg.from,
71
+ subject,
72
+ text: opts.text,
73
+ html: opts.html,
74
+ fromName: opts.fromName,
75
+ inReplyTo: msg.messageId ?? undefined,
76
+ });
77
+ if (result.ok && !msg.read)
78
+ await this.markRead(msg.id).catch(() => { });
79
+ return result;
80
+ }
81
+ /** Internal: call an email API sub-path with the project bearer; throws on error. */
82
+ async call(method, path, body) {
83
+ if (!this.apiUrl || !this.apiToken) {
84
+ throw new Error("Email: not provisioned — EMAIL_API_URL/EMAIL_API_TOKEN missing (redeploy so the platform injects them).");
85
+ }
86
+ const res = await fetch(`${this.apiUrl}/${path}`, {
87
+ method,
88
+ headers: {
89
+ authorization: `Bearer ${this.apiToken}`,
90
+ ...(body ? { "content-type": "application/json" } : {}),
91
+ },
92
+ body: body ? JSON.stringify(body) : undefined,
93
+ });
94
+ const j = (await res.json().catch(() => ({})));
95
+ if (!res.ok || j.error) {
96
+ throw new Error(`Email ${path}: ${String(j.message ?? j.error ?? `HTTP ${res.status}`)}`);
97
+ }
98
+ return j;
99
+ }
100
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { Email } from "./email.js";
3
+ const ENV = { EMAIL_API_URL: "https://site.example/email", EMAIL_API_TOKEN: "tok_123" };
4
+ function mockFetch(handler) {
5
+ const calls = [];
6
+ const fn = vi.fn(async (url, init) => {
7
+ const call = {
8
+ url,
9
+ method: init?.method ?? "GET",
10
+ auth: init?.headers?.authorization ?? null,
11
+ body: init?.body ? JSON.parse(init.body) : undefined,
12
+ };
13
+ calls.push(call);
14
+ const { status = 200, json } = handler(call);
15
+ return { ok: status >= 200 && status < 300, status, json: async () => json };
16
+ });
17
+ vi.stubGlobal("fetch", fn);
18
+ return calls;
19
+ }
20
+ afterEach(() => vi.unstubAllGlobals());
21
+ describe("Email", () => {
22
+ it("lists newest-first, passing the bearer and unread/limit query", async () => {
23
+ const calls = mockFetch(() => ({ json: { ok: true, address: "p@livo.build", messages: [{ id: "e1" }] } }));
24
+ const email = new Email(ENV);
25
+ const msgs = await email.list({ unread: true, limit: 5 });
26
+ expect(msgs).toEqual([{ id: "e1" }]);
27
+ expect(calls[0].url).toBe("https://site.example/email/list?unread=1&limit=5");
28
+ expect(calls[0].method).toBe("GET");
29
+ expect(calls[0].auth).toBe("Bearer tok_123");
30
+ });
31
+ it("send wraps a single recipient into an array and reports the message id", async () => {
32
+ const calls = mockFetch(() => ({ json: { ok: true, message_id: "m_42" } }));
33
+ const email = new Email(ENV);
34
+ const r = await email.send({ to: "a@b.com", subject: "hi", text: "yo" });
35
+ expect(r).toEqual({ ok: true, message_id: "m_42" });
36
+ expect(calls[0].url).toBe("https://site.example/email/send");
37
+ expect(calls[0].body).toMatchObject({ to: ["a@b.com"], subject: "hi", text: "yo" });
38
+ });
39
+ it("reply threads via Message-ID, prefixes Re:, and marks the original read", async () => {
40
+ const calls = mockFetch((c) => (c.url.endsWith("/send") ? { json: { ok: true, message_id: "out1" } } : { json: { ok: true } }));
41
+ const msg = {
42
+ id: "e9", direction: "in", from: "sender@x.com", to: ["p@livo.build"], cc: [],
43
+ subject: "Question", text: "?", html: null, messageId: "<abc@x.com>", inReplyTo: null,
44
+ read: false, receivedAt: 1,
45
+ };
46
+ const email = new Email(ENV);
47
+ const r = await email.reply(msg, { text: "answer" });
48
+ expect(r.ok).toBe(true);
49
+ const send = calls.find((c) => c.url.endsWith("/send"));
50
+ expect(send.body).toMatchObject({ to: ["sender@x.com"], subject: "Re: Question", inReplyTo: "<abc@x.com>" });
51
+ const mark = calls.find((c) => c.url.endsWith("/mark-read"));
52
+ expect(mark?.body).toMatchObject({ id: "e9", read: true });
53
+ });
54
+ it("does not double-prefix Re: on an already-Re subject", async () => {
55
+ const calls = mockFetch(() => ({ json: { ok: true, message_id: "x" } }));
56
+ const msg = { id: "e", direction: "in", from: "s@x.com", to: [], cc: [], subject: "Re: Hi", text: null, html: null, messageId: null, inReplyTo: null, read: true, receivedAt: 0 };
57
+ await new Email(ENV).reply(msg, { text: "k" });
58
+ expect(calls[0].body).toMatchObject({ subject: "Re: Hi" });
59
+ });
60
+ it("throws a helpful error when not provisioned", async () => {
61
+ mockFetch(() => ({ json: {} }));
62
+ await expect(new Email({}).list()).rejects.toThrow(/not provisioned/);
63
+ });
64
+ it("surfaces API errors with the server message", async () => {
65
+ mockFetch(() => ({ status: 403, json: { error: "forbidden", message: "bad token" } }));
66
+ await expect(new Email(ENV).list()).rejects.toThrow(/bad token/);
67
+ });
68
+ });
@@ -0,0 +1,30 @@
1
+ import type { Chain } from "./chain.js";
2
+ import type { Hex } from "./hex.js";
3
+ export type HlNetwork = "mainnet" | "testnet";
4
+ export interface HlBridgeConfig {
5
+ /** Arbitrum chainId the deposit transfer goes out on. */
6
+ chainId: number;
7
+ /** Hyperliquid bridge (Bridge2) contract — the USDC transfer recipient. */
8
+ bridge: Hex;
9
+ /** USDC token on that chain. */
10
+ usdc: Hex;
11
+ usdcDecimals: number;
12
+ /** Hyperliquid enforces a minimum deposit; below it funds are lost. */
13
+ minUsdc: number;
14
+ }
15
+ export declare const HL_BRIDGE: Record<HlNetwork, HlBridgeConfig>;
16
+ /**
17
+ * Convert a human USDC amount (e.g. 25 or "25.5") to base units, without float
18
+ * error. Rejects more fractional digits than the token supports.
19
+ */
20
+ export declare function usdcBaseUnits(amount: number | string, decimals?: number): bigint;
21
+ export interface DepositOptions {
22
+ /** "mainnet" (Arbitrum) or "testnet" (Arbitrum Sepolia). Default "mainnet". */
23
+ network?: HlNetwork;
24
+ }
25
+ /**
26
+ * Deposit USDC into Hyperliquid from Arbitrum. `chain` must be a Chain on the
27
+ * matching Arbitrum network with a funded signer. Returns the deposit tx hash;
28
+ * the credit appears on the signer's Hyperliquid account shortly after.
29
+ */
30
+ export declare function depositToHyperliquid(chain: Chain, amount: number | string, opts?: DepositOptions): Promise<Hex>;
@@ -0,0 +1,61 @@
1
+ // Verify against Hyperliquid docs before relying on the testnet entry; mainnet
2
+ // Bridge2 + native USDC on Arbitrum are the well-known production addresses.
3
+ export const HL_BRIDGE = {
4
+ mainnet: {
5
+ chainId: 42161,
6
+ bridge: "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7",
7
+ usdc: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
8
+ usdcDecimals: 6,
9
+ minUsdc: 5,
10
+ },
11
+ testnet: {
12
+ chainId: 421614,
13
+ bridge: "0x08cfc1B6b2dCF36A1480b99353A354AA8AC56f89",
14
+ usdc: "0x1baAbB04529D43a73232B713C0FE471f7c7334d5",
15
+ usdcDecimals: 6,
16
+ minUsdc: 5,
17
+ },
18
+ };
19
+ const ERC20_TRANSFER = [
20
+ {
21
+ type: "function",
22
+ name: "transfer",
23
+ stateMutability: "nonpayable",
24
+ inputs: [
25
+ { name: "to", type: "address" },
26
+ { name: "amount", type: "uint256" },
27
+ ],
28
+ outputs: [{ name: "", type: "bool" }],
29
+ },
30
+ ];
31
+ /**
32
+ * Convert a human USDC amount (e.g. 25 or "25.5") to base units, without float
33
+ * error. Rejects more fractional digits than the token supports.
34
+ */
35
+ export function usdcBaseUnits(amount, decimals = 6) {
36
+ const s = typeof amount === "number" ? amount.toString() : amount.trim();
37
+ if (!/^\d+(\.\d+)?$/.test(s))
38
+ throw new Error(`invalid USDC amount: ${amount}`);
39
+ const [whole, frac = ""] = s.split(".");
40
+ if (frac.length > decimals)
41
+ throw new Error(`USDC supports at most ${decimals} decimals`);
42
+ return BigInt(whole + frac.padEnd(decimals, "0"));
43
+ }
44
+ /**
45
+ * Deposit USDC into Hyperliquid from Arbitrum. `chain` must be a Chain on the
46
+ * matching Arbitrum network with a funded signer. Returns the deposit tx hash;
47
+ * the credit appears on the signer's Hyperliquid account shortly after.
48
+ */
49
+ export async function depositToHyperliquid(chain, amount, opts = {}) {
50
+ const cfg = HL_BRIDGE[opts.network ?? "mainnet"];
51
+ const units = usdcBaseUnits(amount, cfg.usdcDecimals);
52
+ const min = BigInt(cfg.minUsdc) * 10n ** BigInt(cfg.usdcDecimals);
53
+ if (units < min)
54
+ throw new Error(`Hyperliquid minimum deposit is ${cfg.minUsdc} USDC`);
55
+ return chain.send({
56
+ address: cfg.usdc,
57
+ abi: ERC20_TRANSFER,
58
+ functionName: "transfer",
59
+ args: [cfg.bridge, units],
60
+ });
61
+ }
package/dist/index.d.ts CHANGED
@@ -19,6 +19,8 @@ export { Telegram } from "./telegram.js";
19
19
  export type { TelegramOptions, BotMessage, SendMessageOptions, ChatJoinRequest } from "./telegram.js";
20
20
  export { verifyTelegramInitData, verifyTelegramLoginWidget, parseStartPayload } from "./telegramAuth.js";
21
21
  export type { TelegramUser, VerifyOptions } from "./telegramAuth.js";
22
+ export { Email } from "./email.js";
23
+ export type { EmailOptions, EmailMessage, EmailDirection, ListEmailsOptions, SendEmailOptions, SendEmailResult, } from "./email.js";
22
24
  export { TelegramLinks, TELEGRAM_LINKS_TABLE } from "./telegramLinks.js";
23
25
  export type { TelegramLink } from "./telegramLinks.js";
24
26
  export { tokenBalanceOf, meetsGate } from "./gate.js";
@@ -42,6 +44,8 @@ export { verifyWebhook, signWebhook } from "./webhook.js";
42
44
  export type { VerifyWebhookParams, SignatureEncoding } from "./webhook.js";
43
45
  export { Hyperliquid } from "./hyperliquid.js";
44
46
  export type { HyperliquidOptions, PlaceOrderOptions, MarketOrderOptions, Tif, CandleInterval, CandlesOptions, AssetContext, AccountBalance, PositionInfo, Bbo, } from "./hyperliquid.js";
47
+ export { depositToHyperliquid, usdcBaseUnits, HL_BRIDGE } from "./hyperliquidBridge.js";
48
+ export type { HlNetwork, HlBridgeConfig, DepositOptions } from "./hyperliquidBridge.js";
45
49
  export { Polymarket } from "./polymarket.js";
46
50
  export type { PolymarketOptions, PolymarketCreds, PlaceOrderParams, Side, OrderType, SignatureType, PriceInterval, PriceHistoryOptions, MarketOutcome, ResolvedMarket, } from "./polymarket.js";
47
51
  export { hashTypedData, signTypedData, localAccount } from "./eip712.js";
package/dist/index.js CHANGED
@@ -28,6 +28,8 @@ export { Indexer, indexer, indexerEnvKey, GraphQLError } from "./indexer.js";
28
28
  // Telegram — webhook verification + reply plumbing for bots.
29
29
  export { Telegram } from "./telegram.js";
30
30
  export { verifyTelegramInitData, verifyTelegramLoginWidget, parseStartPayload } from "./telegramAuth.js";
31
+ // Email — the project's own inbox at {slug}@livo.build (read + send/reply).
32
+ export { Email } from "./email.js";
31
33
  export { TelegramLinks, TELEGRAM_LINKS_TABLE } from "./telegramLinks.js";
32
34
  export { tokenBalanceOf, meetsGate } from "./gate.js";
33
35
  export { tokenMetadata, ownerOf, tokenURI } from "./reads.js";
@@ -42,6 +44,8 @@ export { verifyWebhook, signWebhook } from "./webhook.js";
42
44
  // Hyperliquid — perps/spot data + trading (wraps @nktkas/hyperliquid; signs with
43
45
  // the runtime's EIP-712 signer, no viem/ethers in the bundle).
44
46
  export { Hyperliquid } from "./hyperliquid.js";
47
+ // Hyperliquid bridge — deposit USDC from Arbitrum into the L1 account (funds-in).
48
+ export { depositToHyperliquid, usdcBaseUnits, HL_BRIDGE } from "./hyperliquidBridge.js";
45
49
  // Polymarket — prediction-market data (public, frontend-safe) + CLOB order
46
50
  // placement (native EIP-712 + L2 HMAC; no @polymarket/clob-client, no ethers).
47
51
  export { Polymarket } from "./polymarket.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livo-build/runtime",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "description": "Livo runtime — chain signing/reads, D1 state, and logging for keepers, servers, and bots.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",