@livo-build/runtime 0.2.5 → 0.2.12

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.
Files changed (47) hide show
  1. package/dist/aes.d.ts +4 -0
  2. package/dist/aes.js +38 -0
  3. package/dist/auth.test.d.ts +1 -0
  4. package/dist/auth.test.js +57 -0
  5. package/dist/cache.d.ts +12 -0
  6. package/dist/cache.js +27 -0
  7. package/dist/gate.d.ts +10 -0
  8. package/dist/gate.js +18 -0
  9. package/dist/http.d.ts +48 -0
  10. package/dist/http.js +133 -0
  11. package/dist/hyperliquid.d.ts +269 -0
  12. package/dist/hyperliquid.js +194 -0
  13. package/dist/hyperliquid.test.js +17 -1
  14. package/dist/index.d.ts +27 -2
  15. package/dist/index.js +16 -0
  16. package/dist/nft.d.ts +33 -0
  17. package/dist/nft.js +72 -0
  18. package/dist/nft.test.d.ts +1 -0
  19. package/dist/nft.test.js +44 -0
  20. package/dist/polymarket.d.ts +12 -0
  21. package/dist/polymarket.js +19 -6
  22. package/dist/polymarket.test.js +10 -0
  23. package/dist/ratelimit.d.ts +19 -0
  24. package/dist/ratelimit.js +11 -0
  25. package/dist/reads.d.ts +12 -0
  26. package/dist/reads.js +36 -0
  27. package/dist/sessions.d.ts +8 -0
  28. package/dist/sessions.js +60 -0
  29. package/dist/signals.d.ts +131 -0
  30. package/dist/signals.js +146 -0
  31. package/dist/siwe.d.ts +24 -0
  32. package/dist/siwe.js +33 -0
  33. package/dist/sse.test.d.ts +1 -0
  34. package/dist/sse.test.js +28 -0
  35. package/dist/telegram.d.ts +31 -0
  36. package/dist/telegram.js +73 -0
  37. package/dist/telegramAuth.d.ts +16 -0
  38. package/dist/telegramAuth.js +108 -0
  39. package/dist/telegramAuth.test.d.ts +1 -0
  40. package/dist/telegramAuth.test.js +68 -0
  41. package/dist/telegramLinks.d.ts +27 -0
  42. package/dist/telegramLinks.js +78 -0
  43. package/dist/webhook.d.ts +18 -0
  44. package/dist/webhook.js +49 -0
  45. package/dist/webhook.test.d.ts +1 -0
  46. package/dist/webhook.test.js +46 -0
  47. package/package.json +14 -4
@@ -0,0 +1,108 @@
1
+ // Telegram identity verification — the security core of linking a Telegram user to
2
+ // a wallet. Runs server-side (bot token never leaves the server). Two sources:
3
+ // • Mini App initData — verifyTelegramInitData (HMAC keyed by "WebAppData")
4
+ // • Login Widget data — verifyTelegramLoginWidget (HMAC keyed by SHA256(token))
5
+ // Both follow Telegram's documented algorithm exactly: build a data_check_string of
6
+ // the fields (sorted, key=value, "\n"-joined, EXCLUDING `hash`), HMAC-SHA256 it, and
7
+ // constant-time compare to the provided `hash`. Web Crypto only (Worker-safe).
8
+ // Bare lowercase hex (NO "0x" prefix) — Telegram's `hash` is bare hex, so we must
9
+ // not use the chain-style bytesToHex (which prefixes "0x").
10
+ function toHex(bytes) {
11
+ let out = "";
12
+ for (let i = 0; i < bytes.length; i++)
13
+ out += bytes[i].toString(16).padStart(2, "0");
14
+ return out;
15
+ }
16
+ const enc = (s) => new TextEncoder().encode(s);
17
+ async function hmacSha256(keyBytes, msg) {
18
+ const key = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
19
+ return new Uint8Array(await crypto.subtle.sign("HMAC", key, msg));
20
+ }
21
+ async function sha256(bytes) {
22
+ return new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
23
+ }
24
+ // Constant-time-ish hex compare (avoids leaking the match position).
25
+ function safeEqualHex(a, b) {
26
+ if (a.length !== b.length)
27
+ return false;
28
+ let diff = 0;
29
+ for (let i = 0; i < a.length; i++)
30
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
31
+ return diff === 0;
32
+ }
33
+ function freshEnough(authDate, maxAgeSeconds) {
34
+ if (maxAgeSeconds <= 0)
35
+ return true;
36
+ if (!authDate)
37
+ return false;
38
+ return Math.floor(Date.now() / 1000) - authDate <= maxAgeSeconds;
39
+ }
40
+ function userFromRaw(u) {
41
+ return {
42
+ id: Number(u.id),
43
+ username: u.username,
44
+ firstName: u.first_name,
45
+ lastName: u.last_name,
46
+ languageCode: u.language_code,
47
+ isPremium: u.is_premium,
48
+ photoUrl: u.photo_url,
49
+ };
50
+ }
51
+ // Verify Telegram Mini App `initData` (the raw query string from
52
+ // window.Telegram.WebApp.initData). Returns the verified user, or null.
53
+ export async function verifyTelegramInitData(initData, botToken, options = {}) {
54
+ if (!initData || !botToken)
55
+ return null;
56
+ const params = new URLSearchParams(initData);
57
+ const hash = params.get("hash");
58
+ if (!hash)
59
+ return null;
60
+ params.delete("hash");
61
+ const dataCheckString = [...params.entries()]
62
+ .map(([k, v]) => `${k}=${v}`)
63
+ .sort()
64
+ .join("\n");
65
+ // secret_key = HMAC_SHA256(key="WebAppData", message=bot_token)
66
+ const secretKey = await hmacSha256(enc("WebAppData"), enc(botToken));
67
+ const calc = toHex(await hmacSha256(secretKey, enc(dataCheckString)));
68
+ if (!safeEqualHex(calc, hash))
69
+ return null;
70
+ if (!freshEnough(Number(params.get("auth_date") ?? 0), options.maxAgeSeconds ?? 86400))
71
+ return null;
72
+ const userJson = params.get("user");
73
+ if (!userJson)
74
+ return null;
75
+ try {
76
+ return userFromRaw(JSON.parse(userJson));
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ // Verify Telegram Login Widget data (the object the widget hands your page, with a
83
+ // `hash`). Returns the verified user, or null.
84
+ export async function verifyTelegramLoginWidget(data, botToken, options = {}) {
85
+ const hash = data.hash;
86
+ if (!hash || !botToken)
87
+ return null;
88
+ const dataCheckString = Object.keys(data)
89
+ .filter((k) => k !== "hash" && data[k] !== undefined)
90
+ .sort()
91
+ .map((k) => `${k}=${data[k]}`)
92
+ .join("\n");
93
+ // secret_key = SHA256(bot_token) (NOTE: plain SHA-256 here, not HMAC).
94
+ const secretKey = await sha256(enc(botToken));
95
+ const calc = toHex(await hmacSha256(secretKey, enc(dataCheckString)));
96
+ if (!safeEqualHex(calc, String(hash)))
97
+ return null;
98
+ if (!freshEnough(Number(data.auth_date ?? 0), options.maxAgeSeconds ?? 86400))
99
+ return null;
100
+ return userFromRaw(data);
101
+ }
102
+ // Pull the payload from a `/start <payload>` deep-link command (the non-Mini-App
103
+ // linking flow). Returns the trimmed payload, or null.
104
+ export function parseStartPayload(text) {
105
+ const m = /^\/start(?:@\w+)?(?:\s+(.+))?$/.exec(text.trim());
106
+ const payload = m?.[1]?.trim();
107
+ return payload ? payload : null;
108
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createHmac, createHash } from "node:crypto";
3
+ import { verifyTelegramInitData, verifyTelegramLoginWidget, parseStartPayload } from "./telegramAuth.js";
4
+ const BOT_TOKEN = "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11";
5
+ const USER = { id: 42, username: "alice", first_name: "Alice", language_code: "en" };
6
+ // Build a valid Mini App initData the way Telegram does — using node:crypto, an
7
+ // INDEPENDENT implementation from the verifier's Web Crypto. Agreement = correct.
8
+ function makeInitData(botToken, authDate, user = USER) {
9
+ const params = new URLSearchParams();
10
+ params.set("user", JSON.stringify(user));
11
+ params.set("auth_date", String(authDate));
12
+ params.set("query_id", "AAH-test");
13
+ const dcs = [...params.entries()].map(([k, v]) => `${k}=${v}`).sort().join("\n");
14
+ const secret = createHmac("sha256", "WebAppData").update(botToken).digest();
15
+ const hash = createHmac("sha256", secret).update(dcs).digest("hex");
16
+ params.set("hash", hash);
17
+ return params.toString();
18
+ }
19
+ const now = () => Math.floor(Date.now() / 1000);
20
+ describe("verifyTelegramInitData", () => {
21
+ it("accepts a valid initData and returns the user", async () => {
22
+ const u = await verifyTelegramInitData(makeInitData(BOT_TOKEN, now()), BOT_TOKEN);
23
+ expect(u).not.toBeNull();
24
+ expect(u?.id).toBe(42);
25
+ expect(u?.username).toBe("alice");
26
+ expect(u?.firstName).toBe("Alice");
27
+ });
28
+ it("rejects a tampered hash", async () => {
29
+ const good = makeInitData(BOT_TOKEN, now());
30
+ const tampered = good.replace(/hash=([0-9a-f])/, (_, c) => `hash=${c === "0" ? "1" : "0"}`);
31
+ expect(await verifyTelegramInitData(tampered, BOT_TOKEN)).toBeNull();
32
+ });
33
+ it("rejects the wrong bot token", async () => {
34
+ expect(await verifyTelegramInitData(makeInitData(BOT_TOKEN, now()), "999:WRONG")).toBeNull();
35
+ });
36
+ it("rejects stale auth_date", async () => {
37
+ const old = makeInitData(BOT_TOKEN, now() - 100_000);
38
+ expect(await verifyTelegramInitData(old, BOT_TOKEN, { maxAgeSeconds: 3600 })).toBeNull();
39
+ // ...but accepts it when the age check is disabled.
40
+ expect(await verifyTelegramInitData(old, BOT_TOKEN, { maxAgeSeconds: 0 })).not.toBeNull();
41
+ });
42
+ it("rejects empty / hashless input", async () => {
43
+ expect(await verifyTelegramInitData("", BOT_TOKEN)).toBeNull();
44
+ expect(await verifyTelegramInitData("user=%7B%7D&auth_date=1", BOT_TOKEN)).toBeNull();
45
+ });
46
+ });
47
+ describe("verifyTelegramLoginWidget", () => {
48
+ it("accepts valid widget data (SHA256(token) secret)", async () => {
49
+ const data = { id: 42, username: "alice", auth_date: now() };
50
+ const dcs = Object.keys(data).sort().map((k) => `${k}=${data[k]}`).join("\n");
51
+ const secret = createHash("sha256").update(BOT_TOKEN).digest();
52
+ data.hash = createHmac("sha256", secret).update(dcs).digest("hex");
53
+ const u = await verifyTelegramLoginWidget(data, BOT_TOKEN);
54
+ expect(u?.id).toBe(42);
55
+ expect(u?.username).toBe("alice");
56
+ });
57
+ it("rejects a bad hash", async () => {
58
+ expect(await verifyTelegramLoginWidget({ id: 1, auth_date: now(), hash: "deadbeef" }, BOT_TOKEN)).toBeNull();
59
+ });
60
+ });
61
+ describe("parseStartPayload", () => {
62
+ it("extracts the payload from /start", () => {
63
+ expect(parseStartPayload("/start link_abc123")).toBe("link_abc123");
64
+ expect(parseStartPayload("/start@MyBot tok")).toBe("tok");
65
+ expect(parseStartPayload("/start")).toBeNull();
66
+ expect(parseStartPayload("hello")).toBeNull();
67
+ });
68
+ });
@@ -0,0 +1,27 @@
1
+ import type { D1Like } from "./store.js";
2
+ export declare const TELEGRAM_LINKS_TABLE = "telegram_links";
3
+ export interface TelegramLink {
4
+ telegramId: string;
5
+ address: string;
6
+ username?: string;
7
+ createdAt: number;
8
+ }
9
+ export declare class TelegramLinks {
10
+ private readonly db;
11
+ private readonly table;
12
+ private ready;
13
+ constructor(db: D1Like, table?: string);
14
+ private init;
15
+ /** Create/replace the link for a Telegram user (one wallet per Telegram id). */
16
+ link(telegramId: string | number, address: string, username?: string): Promise<void>;
17
+ /** Remove the link for a Telegram user. */
18
+ unlink(telegramId: string | number): Promise<void>;
19
+ /** The wallet address linked to a Telegram user, or null. */
20
+ walletFor(telegramId: string | number): Promise<string | null>;
21
+ /** The Telegram link for a wallet address, or null. */
22
+ telegramFor(address: string): Promise<TelegramLink | null>;
23
+ /** Whether a Telegram user has a linked wallet. */
24
+ isLinked(telegramId: string | number): Promise<boolean>;
25
+ /** Every link — for a keeper that re-checks balances and boots ineligible users. */
26
+ all(limit?: number): Promise<TelegramLink[]>;
27
+ }
@@ -0,0 +1,78 @@
1
+ // TelegramLinks — the shared binding between a Telegram user and a wallet address,
2
+ // stored in the project's D1 (env.DB, which is bound into BOTH the api Worker and
3
+ // the bot). Write it from the api after verifying both proofs; read it from the bot
4
+ // to answer "is this user's wallet connected?".
5
+ export const TELEGRAM_LINKS_TABLE = "telegram_links";
6
+ export class TelegramLinks {
7
+ db;
8
+ table;
9
+ ready = null;
10
+ constructor(db, table = TELEGRAM_LINKS_TABLE) {
11
+ this.db = db;
12
+ this.table = table;
13
+ if (!db)
14
+ throw new Error("TelegramLinks: no D1 binding — pass env.DB (is the project's D1 bound?)");
15
+ }
16
+ async init() {
17
+ if (!this.ready) {
18
+ this.ready = (async () => {
19
+ if (this.db.exec) {
20
+ await this.db.exec(`CREATE TABLE IF NOT EXISTS ${this.table} (telegram_id TEXT PRIMARY KEY, address TEXT NOT NULL, username TEXT, created_at INTEGER); CREATE INDEX IF NOT EXISTS ${this.table}_address ON ${this.table} (address);`);
21
+ }
22
+ })();
23
+ }
24
+ return this.ready;
25
+ }
26
+ /** Create/replace the link for a Telegram user (one wallet per Telegram id). */
27
+ async link(telegramId, address, username) {
28
+ await this.init();
29
+ await this.db
30
+ .prepare(`INSERT INTO ${this.table} (telegram_id, address, username, created_at) VALUES (?, ?, ?, ?)
31
+ ON CONFLICT(telegram_id) DO UPDATE SET address = excluded.address, username = excluded.username, created_at = excluded.created_at`)
32
+ .bind(String(telegramId), address.toLowerCase(), username ?? null, Date.now())
33
+ .run();
34
+ }
35
+ /** Remove the link for a Telegram user. */
36
+ async unlink(telegramId) {
37
+ await this.init();
38
+ await this.db.prepare(`DELETE FROM ${this.table} WHERE telegram_id = ?`).bind(String(telegramId)).run();
39
+ }
40
+ /** The wallet address linked to a Telegram user, or null. */
41
+ async walletFor(telegramId) {
42
+ await this.init();
43
+ const row = await this.db
44
+ .prepare(`SELECT address FROM ${this.table} WHERE telegram_id = ?`)
45
+ .bind(String(telegramId))
46
+ .first();
47
+ return row ? row.address : null;
48
+ }
49
+ /** The Telegram link for a wallet address, or null. */
50
+ async telegramFor(address) {
51
+ await this.init();
52
+ const row = await this.db
53
+ .prepare(`SELECT telegram_id, address, username, created_at FROM ${this.table} WHERE address = ?`)
54
+ .bind(address.toLowerCase())
55
+ .first();
56
+ return row
57
+ ? { telegramId: row.telegram_id, address: row.address, username: row.username ?? undefined, createdAt: row.created_at }
58
+ : null;
59
+ }
60
+ /** Whether a Telegram user has a linked wallet. */
61
+ async isLinked(telegramId) {
62
+ return (await this.walletFor(telegramId)) !== null;
63
+ }
64
+ /** Every link — for a keeper that re-checks balances and boots ineligible users. */
65
+ async all(limit = 1000) {
66
+ await this.init();
67
+ const res = await this.db
68
+ .prepare(`SELECT telegram_id, address, username, created_at FROM ${this.table} LIMIT ?`)
69
+ .bind(limit)
70
+ .all();
71
+ return (res.results ?? []).map((r) => ({
72
+ telegramId: r.telegram_id,
73
+ address: r.address,
74
+ username: r.username ?? undefined,
75
+ createdAt: r.created_at,
76
+ }));
77
+ }
78
+ }
@@ -0,0 +1,18 @@
1
+ export type SignatureEncoding = "hex" | "base64";
2
+ /** Compute the HMAC-SHA256 signature of a payload (for signing outgoing webhooks/tests). */
3
+ export declare function signWebhook(payload: string, secret: string, encoding?: SignatureEncoding): Promise<string>;
4
+ export interface VerifyWebhookParams {
5
+ /** The RAW request body (verify before JSON.parse — re-serialization changes bytes). */
6
+ payload: string;
7
+ secret: string;
8
+ /** The signature header value. A `scheme=` prefix (e.g. "sha256=") is stripped for hex. */
9
+ signature: string;
10
+ /** Digest encoding of `signature` (default "hex"). */
11
+ encoding?: SignatureEncoding;
12
+ }
13
+ /**
14
+ * Verify a webhook's HMAC-SHA256 signature against the raw body. Returns true iff the
15
+ * signature matches. For hex signatures a leading `algo=` prefix (GitHub's "sha256=")
16
+ * is stripped automatically.
17
+ */
18
+ export declare function verifyWebhook(params: VerifyWebhookParams): Promise<boolean>;
@@ -0,0 +1,49 @@
1
+ // Inbound webhook signature verification (GitHub/Stripe-style HMAC-SHA256). Verify
2
+ // that a raw request body was signed with your shared secret before trusting it.
3
+ // Constant-time comparison; no hand-rolled crypto.
4
+ function enc(s) {
5
+ return new TextEncoder().encode(s);
6
+ }
7
+ function toHex(bytes) {
8
+ let out = "";
9
+ for (const b of bytes)
10
+ out += b.toString(16).padStart(2, "0");
11
+ return out;
12
+ }
13
+ function toBase64(bytes) {
14
+ let bin = "";
15
+ for (const b of bytes)
16
+ bin += String.fromCharCode(b);
17
+ return btoa(bin);
18
+ }
19
+ async function hmac(secret, payload) {
20
+ const key = await crypto.subtle.importKey("raw", enc(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
21
+ return new Uint8Array(await crypto.subtle.sign("HMAC", key, enc(payload)));
22
+ }
23
+ /** Constant-time string compare (length-independent leak is acceptable for digests). */
24
+ function timingSafeEqual(a, b) {
25
+ if (a.length !== b.length)
26
+ return false;
27
+ let diff = 0;
28
+ for (let i = 0; i < a.length; i++)
29
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
30
+ return diff === 0;
31
+ }
32
+ /** Compute the HMAC-SHA256 signature of a payload (for signing outgoing webhooks/tests). */
33
+ export async function signWebhook(payload, secret, encoding = "hex") {
34
+ const mac = await hmac(secret, payload);
35
+ return encoding === "base64" ? toBase64(mac) : toHex(mac);
36
+ }
37
+ /**
38
+ * Verify a webhook's HMAC-SHA256 signature against the raw body. Returns true iff the
39
+ * signature matches. For hex signatures a leading `algo=` prefix (GitHub's "sha256=")
40
+ * is stripped automatically.
41
+ */
42
+ export async function verifyWebhook(params) {
43
+ const encoding = params.encoding ?? "hex";
44
+ let provided = params.signature.trim();
45
+ if (encoding === "hex")
46
+ provided = provided.replace(/^[A-Za-z0-9_-]+=/, ""); // strip "sha256=" etc (hex has no '=')
47
+ const expected = await signWebhook(params.payload, params.secret, encoding);
48
+ return timingSafeEqual(expected.toLowerCase(), provided.toLowerCase());
49
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { signWebhook, verifyWebhook } from "./webhook.js";
3
+ import { cached } from "./cache.js";
4
+ describe("webhook signatures", () => {
5
+ it("round-trips a hex signature and strips a sha256= prefix", async () => {
6
+ const sig = await signWebhook("payload-body", "shh");
7
+ expect(await verifyWebhook({ payload: "payload-body", secret: "shh", signature: sig })).toBe(true);
8
+ expect(await verifyWebhook({ payload: "payload-body", secret: "shh", signature: "sha256=" + sig })).toBe(true);
9
+ });
10
+ it("rejects a wrong secret or tampered payload", async () => {
11
+ const sig = await signWebhook("a", "s1");
12
+ expect(await verifyWebhook({ payload: "a", secret: "s2", signature: sig })).toBe(false);
13
+ expect(await verifyWebhook({ payload: "b", secret: "s1", signature: sig })).toBe(false);
14
+ });
15
+ it("supports base64 encoding", async () => {
16
+ const sig = await signWebhook("x", "k", "base64");
17
+ expect(await verifyWebhook({ payload: "x", secret: "k", signature: sig, encoding: "base64" })).toBe(true);
18
+ });
19
+ });
20
+ function memKv() {
21
+ const store = new Map();
22
+ return {
23
+ store,
24
+ get: async (k) => store.get(k) ?? null,
25
+ put: async (k, v) => void store.set(k, v),
26
+ };
27
+ }
28
+ describe("cached", () => {
29
+ it("computes once, then serves from cache", async () => {
30
+ const kv = memKv();
31
+ let calls = 0;
32
+ const produce = async () => {
33
+ calls++;
34
+ return { n: 42 };
35
+ };
36
+ expect(await cached(kv, "k", produce, { ttlSeconds: 60 })).toEqual({ n: 42 });
37
+ expect(await cached(kv, "k", produce, { ttlSeconds: 60 })).toEqual({ n: 42 });
38
+ expect(calls).toBe(1);
39
+ expect(kv.store.has("cache:k")).toBe(true);
40
+ });
41
+ it("recomputes when the cached value is corrupt", async () => {
42
+ const kv = memKv();
43
+ kv.store.set("cache:bad", "{not json");
44
+ expect(await cached(kv, "bad", async () => "fresh", { ttlSeconds: 60 })).toBe("fresh");
45
+ });
46
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livo-build/runtime",
3
- "version": "0.2.5",
3
+ "version": "0.2.12",
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",
@@ -16,9 +16,19 @@
16
16
  "import": "./dist/contracts.js"
17
17
  }
18
18
  },
19
- "files": ["dist"],
20
- "engines": { "node": ">=18" },
21
- "keywords": ["livo", "web3", "keeper", "eip1559", "evm"],
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "keywords": [
26
+ "livo",
27
+ "web3",
28
+ "keeper",
29
+ "eip1559",
30
+ "evm"
31
+ ],
22
32
  "publishConfig": {
23
33
  "access": "public"
24
34
  },