@tuwaio/quasar-sdk 0.1.8 → 0.1.10
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/README.md +82 -0
- package/dist/cli.cjs +467 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ It operates strictly on the server (Node.js, Next.js Server Actions, or Edge fun
|
|
|
22
22
|
## ✨ Key Features
|
|
23
23
|
|
|
24
24
|
- **☁️ Cloud Sync**: Automatically persist pending and terminal transaction states to the Quasar Database for cross-device history.
|
|
25
|
+
- **⚡ Native Webhook Local Dev Relay**: Stream production webhooks directly to your local machine via CLI with zero tunnels (no ngrok/cloudflared required).
|
|
25
26
|
- **🔐 Headless SIWX (CAIP-122) Auth Ready**: Seamlessly pairs with `@tuwaio/sdk/siwx/server` for strict cryptographic verification of user sessions before allowing database writes.
|
|
26
27
|
- **⚡ Edge Ready**: Uses `ofetch` and lightweight cryptography to run seamlessly in Cloudflare Workers and Vercel Edge.
|
|
27
28
|
- **📦 InMemory Sync**: Perfectly pairs with `@tuwaio/sdk/pulsar` (`createTxInMemoryStore`) to fetch history and hydrate local React states.
|
|
@@ -38,6 +39,87 @@ _Note: `ofetch` and `@tuwaio/pulsar-core` are required peer dependencies. The `@
|
|
|
38
39
|
|
|
39
40
|
---
|
|
40
41
|
|
|
42
|
+
## ⚡ Quasar Webhook Local Dev Relay (Native CLI)
|
|
43
|
+
|
|
44
|
+
Testing Web3 webhooks locally against live cloud indexers usually requires spinning up temporary third-party tunnels (`ngrok`, `cloudflared`), pasting ephemeral URLs into dashboard forms, and restarting your workflow when tunnels expire.
|
|
45
|
+
|
|
46
|
+
The **Quasar SDK CLI** provides a zero-tunnel local development relay. When you configure a `localhost` webhook endpoint in the Quasar Dashboard, Quasar Cloud streams incoming events to your local machine via real-time Server-Sent Events (SSE).
|
|
47
|
+
|
|
48
|
+
### 1. Zero-Config Local Development
|
|
49
|
+
|
|
50
|
+
Add your webhook signing secret from the Quasar Dashboard to `.env.local` or `.env`:
|
|
51
|
+
|
|
52
|
+
```env
|
|
53
|
+
# Signing secret copied from the Quasar Dashboard webhook endpoint
|
|
54
|
+
QUASAR_WEBHOOK_SECRET=whsec_...
|
|
55
|
+
|
|
56
|
+
# Optional Quasar API base URL (defaults to https://api.tuwa.io)
|
|
57
|
+
NEXT_PUBLIC_QUASAR_BASE_URL=https://api.tuwa.io
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Run the relay listener in your terminal:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx @tuwaio/quasar-sdk listen
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The CLI automatically reads `QUASAR_WEBHOOK_SECRET` and starts forwarding webhook payloads to the default Cosmos Playground endpoint (`http://localhost:3000/api/webhooks/quasar`).
|
|
67
|
+
|
|
68
|
+
### 2. Custom Forwarding & CLI Options
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Forward to a custom port or path
|
|
72
|
+
npx @tuwaio/quasar-sdk listen --forward-to http://localhost:8080/api/webhooks
|
|
73
|
+
|
|
74
|
+
# Provide secret explicitly via flags
|
|
75
|
+
npx @tuwaio/quasar-sdk listen --secret whsec_... --forward-to http://localhost:3000/api/webhooks/quasar
|
|
76
|
+
|
|
77
|
+
# Specify custom .env file
|
|
78
|
+
npx @tuwaio/quasar-sdk listen --env-file .env.development
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| Flag | Shorthand | Description | Default |
|
|
82
|
+
| :------------- | :-------- | :-------------------------------------------- | :------------------------------------------ |
|
|
83
|
+
| `--secret` | `-s` | Webhook endpoint signing secret (`whsec_...`) | `QUASAR_WEBHOOK_SECRET` |
|
|
84
|
+
| `--forward-to` | `-f` | Local destination endpoint | `http://localhost:3000/api/webhooks/quasar` |
|
|
85
|
+
| `--api-url` | `-a` | Quasar Cloud API base URL | `https://api.tuwa.io` |
|
|
86
|
+
| `--env-file` | `-e` | Custom path to `.env` file | Auto-detected (`.env.local`, `.env`) |
|
|
87
|
+
| `--help` | `-h` | Display help instructions | — |
|
|
88
|
+
| `--version` | `-v` | Display CLI version | — |
|
|
89
|
+
|
|
90
|
+
### 3. Local Webhook Route Handler (Next.js Example)
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// app/api/webhooks/quasar/route.ts
|
|
94
|
+
import { NextResponse } from 'next/server';
|
|
95
|
+
import crypto from 'node:crypto';
|
|
96
|
+
|
|
97
|
+
export async function POST(req: Request) {
|
|
98
|
+
const secret = process.env.QUASAR_WEBHOOK_SECRET;
|
|
99
|
+
if (!secret) {
|
|
100
|
+
return NextResponse.json({ error: 'Server misconfigured' }, { status: 500 });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const rawBody = await req.text();
|
|
104
|
+
const signature = req.headers.get('x-quasar-signature');
|
|
105
|
+
const event = req.headers.get('x-quasar-event');
|
|
106
|
+
|
|
107
|
+
// Verify HMAC-SHA256 signature
|
|
108
|
+
const expectedSignature = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
|
|
109
|
+
|
|
110
|
+
if (signature !== expectedSignature) {
|
|
111
|
+
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const payload = JSON.parse(rawBody);
|
|
115
|
+
console.log(`[Webhook Received] ${event}:`, payload);
|
|
116
|
+
|
|
117
|
+
return NextResponse.json({ received: true });
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
41
123
|
## 🚀 Quick Start (Node.js / Edge)
|
|
42
124
|
|
|
43
125
|
This is a basic example of how to interact with the Quasar Cloud directly from your secure backend environments (like Next.js API Routes, Server Actions, or NestJS).
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/cli.ts
|
|
32
|
+
var cli_exports = {};
|
|
33
|
+
__export(cli_exports, {
|
|
34
|
+
CLI_VERSION: () => CLI_VERSION,
|
|
35
|
+
DEFAULT_API_URL: () => DEFAULT_API_URL,
|
|
36
|
+
DEFAULT_FORWARD_URL: () => DEFAULT_FORWARD_URL,
|
|
37
|
+
handleSseMessage: () => handleSseMessage,
|
|
38
|
+
loadEnv: () => loadEnv,
|
|
39
|
+
main: () => main,
|
|
40
|
+
parseArgs: () => parseArgs,
|
|
41
|
+
parseEnvFile: () => parseEnvFile
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(cli_exports);
|
|
44
|
+
var fs = __toESM(require("fs"), 1);
|
|
45
|
+
var path = __toESM(require("path"), 1);
|
|
46
|
+
|
|
47
|
+
// package.json
|
|
48
|
+
var package_default = {
|
|
49
|
+
name: "@tuwaio/quasar-sdk",
|
|
50
|
+
version: "0.1.10",
|
|
51
|
+
private: false,
|
|
52
|
+
author: "Oleksandr Tkach",
|
|
53
|
+
license: "Apache-2.0",
|
|
54
|
+
description: "Layer 5 (L5) of the TUWA Ecosystem. The Quasar Cloud SDK providing server-side Node.js & Edge utilities for transaction indexing, billing, and backend API integration.",
|
|
55
|
+
type: "module",
|
|
56
|
+
main: "./dist/index.cjs",
|
|
57
|
+
module: "./dist/index.js",
|
|
58
|
+
types: "./dist/index.d.ts",
|
|
59
|
+
exports: {
|
|
60
|
+
".": {
|
|
61
|
+
types: "./dist/index.d.ts",
|
|
62
|
+
import: "./dist/index.js",
|
|
63
|
+
require: "./dist/index.cjs"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
publishConfig: {
|
|
67
|
+
access: "public"
|
|
68
|
+
},
|
|
69
|
+
files: [
|
|
70
|
+
"dist",
|
|
71
|
+
"socket.json"
|
|
72
|
+
],
|
|
73
|
+
bin: {
|
|
74
|
+
"@tuwaio/quasar-sdk": "./dist/cli.cjs",
|
|
75
|
+
"quasar-sdk": "./dist/cli.cjs"
|
|
76
|
+
},
|
|
77
|
+
scripts: {
|
|
78
|
+
start: "tsup src/index.ts --watch",
|
|
79
|
+
build: "tsup",
|
|
80
|
+
test: "vitest run"
|
|
81
|
+
},
|
|
82
|
+
keywords: [
|
|
83
|
+
"tuwa",
|
|
84
|
+
"web3",
|
|
85
|
+
"evm",
|
|
86
|
+
"ethereum",
|
|
87
|
+
"solana",
|
|
88
|
+
"wagmi",
|
|
89
|
+
"viem",
|
|
90
|
+
"@solana/kit",
|
|
91
|
+
"wallet-standard",
|
|
92
|
+
"wallet",
|
|
93
|
+
"wallet-connect",
|
|
94
|
+
"wallets",
|
|
95
|
+
"transaction",
|
|
96
|
+
"tracking",
|
|
97
|
+
"transaction-tracking",
|
|
98
|
+
"react",
|
|
99
|
+
"ui",
|
|
100
|
+
"ui-kit",
|
|
101
|
+
"design-system",
|
|
102
|
+
"hooks",
|
|
103
|
+
"utils",
|
|
104
|
+
"components",
|
|
105
|
+
"tailwind",
|
|
106
|
+
"tailwindcss",
|
|
107
|
+
"typescript",
|
|
108
|
+
"core",
|
|
109
|
+
"headless",
|
|
110
|
+
"zustand",
|
|
111
|
+
"immer",
|
|
112
|
+
"i18n",
|
|
113
|
+
"modal",
|
|
114
|
+
"toast",
|
|
115
|
+
"quasar",
|
|
116
|
+
"cloud",
|
|
117
|
+
"sdk",
|
|
118
|
+
"next-auth",
|
|
119
|
+
"cosmos",
|
|
120
|
+
"playground",
|
|
121
|
+
"cli"
|
|
122
|
+
],
|
|
123
|
+
repository: {
|
|
124
|
+
type: "git",
|
|
125
|
+
url: "git+https://github.com/TuwaIO/sdk.git",
|
|
126
|
+
directory: "packages/quasar-sdk"
|
|
127
|
+
},
|
|
128
|
+
homepage: "https://github.com/TuwaIO/sdk",
|
|
129
|
+
bugs: {
|
|
130
|
+
url: "https://github.com/TuwaIO/sdk/issues"
|
|
131
|
+
},
|
|
132
|
+
contributors: [
|
|
133
|
+
{
|
|
134
|
+
name: "Oleksandr Tkach",
|
|
135
|
+
url: "https://github.com/Argeare5"
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
peerDependencies: {
|
|
139
|
+
"@tuwaio/pulsar-core": ">=0.7",
|
|
140
|
+
ofetch: "^1.5.1",
|
|
141
|
+
"@tuwaio/siwx-react": ">=0.3",
|
|
142
|
+
"@tuwaio/siwx-core": ">=0.3",
|
|
143
|
+
"@tuwaio/siwx-server": ">=0.3"
|
|
144
|
+
},
|
|
145
|
+
peerDependenciesMeta: {
|
|
146
|
+
"@tuwaio/pulsar-core": {
|
|
147
|
+
optional: false
|
|
148
|
+
},
|
|
149
|
+
ofetch: {
|
|
150
|
+
optional: false
|
|
151
|
+
},
|
|
152
|
+
"@tuwaio/siwx-core": {
|
|
153
|
+
optional: true
|
|
154
|
+
},
|
|
155
|
+
"@tuwaio/siwx-server": {
|
|
156
|
+
optional: true
|
|
157
|
+
},
|
|
158
|
+
"@tuwaio/siwx-react": {
|
|
159
|
+
optional: true
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
devDependencies: {
|
|
163
|
+
"@tuwaio/pulsar-core": "^0.7.0",
|
|
164
|
+
ofetch: "^1.5.1",
|
|
165
|
+
tsup: "^8.5.1",
|
|
166
|
+
typescript: "6.0.3",
|
|
167
|
+
"@tuwaio/siwx-react": "^0.3.0",
|
|
168
|
+
"@tuwaio/siwx-core": "^0.3.0",
|
|
169
|
+
"@tuwaio/siwx-server": "^0.3.0"
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// src/cli.ts
|
|
174
|
+
var DEFAULT_FORWARD_URL = "http://localhost:3000/api/webhooks/quasar";
|
|
175
|
+
var DEFAULT_API_URL = "https://api.tuwa.io";
|
|
176
|
+
var CLI_VERSION = package_default.version || "0.0.0";
|
|
177
|
+
function parseEnvFile(content) {
|
|
178
|
+
const env = {};
|
|
179
|
+
const lines = content.split("\n");
|
|
180
|
+
for (const line of lines) {
|
|
181
|
+
const trimmed = line.trim();
|
|
182
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
183
|
+
const equalsIdx = trimmed.indexOf("=");
|
|
184
|
+
if (equalsIdx <= 0) continue;
|
|
185
|
+
const key = trimmed.slice(0, equalsIdx).trim();
|
|
186
|
+
let val = trimmed.slice(equalsIdx + 1).trim();
|
|
187
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
188
|
+
val = val.slice(1, -1);
|
|
189
|
+
}
|
|
190
|
+
env[key] = val;
|
|
191
|
+
}
|
|
192
|
+
return env;
|
|
193
|
+
}
|
|
194
|
+
function loadEnv(customPath) {
|
|
195
|
+
const cwd = process.cwd();
|
|
196
|
+
if (customPath) {
|
|
197
|
+
const fullPath = path.isAbsolute(customPath) ? customPath : path.join(cwd, customPath);
|
|
198
|
+
if (fs.existsSync(fullPath)) {
|
|
199
|
+
return parseEnvFile(fs.readFileSync(fullPath, "utf8"));
|
|
200
|
+
}
|
|
201
|
+
console.warn(`\x1B[33m[warn]\x1B[0m Specified env file not found: ${customPath}`);
|
|
202
|
+
return {};
|
|
203
|
+
}
|
|
204
|
+
const candidates = [".env.local", ".env.development", ".env"];
|
|
205
|
+
for (const candidate of candidates) {
|
|
206
|
+
const filePath = path.join(cwd, candidate);
|
|
207
|
+
if (fs.existsSync(filePath)) {
|
|
208
|
+
return parseEnvFile(fs.readFileSync(filePath, "utf8"));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
function parseArgs(args) {
|
|
214
|
+
const options = {};
|
|
215
|
+
const cleanArgs = [...args];
|
|
216
|
+
if (cleanArgs.length > 0 && !cleanArgs[0].startsWith("-")) {
|
|
217
|
+
options.command = cleanArgs.shift();
|
|
218
|
+
}
|
|
219
|
+
for (let i = 0; i < cleanArgs.length; i++) {
|
|
220
|
+
const arg = cleanArgs[i];
|
|
221
|
+
if (arg === "--help" || arg === "-h") {
|
|
222
|
+
options.help = true;
|
|
223
|
+
} else if (arg === "--version" || arg === "-v") {
|
|
224
|
+
options.version = true;
|
|
225
|
+
} else if (arg === "--secret" || arg === "-s") {
|
|
226
|
+
options.secret = cleanArgs[++i];
|
|
227
|
+
} else if (arg === "--forward-to" || arg === "-f") {
|
|
228
|
+
options.forwardTo = cleanArgs[++i];
|
|
229
|
+
} else if (arg === "--api-url" || arg === "-a") {
|
|
230
|
+
options.apiUrl = cleanArgs[++i];
|
|
231
|
+
} else if (arg === "--env-file" || arg === "-e") {
|
|
232
|
+
options.envFile = cleanArgs[++i];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return options;
|
|
236
|
+
}
|
|
237
|
+
function printHelp() {
|
|
238
|
+
console.log(`
|
|
239
|
+
\x1B[1m\x1B[36m\u26A1 Quasar Webhook Local Dev Relay\x1B[0m
|
|
240
|
+
Stream production Quasar Cloud webhooks directly to your local dev machine.
|
|
241
|
+
Zero tunnels, zero third-party proxies, zero friction.
|
|
242
|
+
|
|
243
|
+
\x1B[1mUSAGE\x1B[0m
|
|
244
|
+
$ npx @tuwaio/quasar-sdk listen [options]
|
|
245
|
+
|
|
246
|
+
\x1B[1mCOMMANDS\x1B[0m
|
|
247
|
+
listen Connect to Quasar Cloud SSE and forward events locally
|
|
248
|
+
|
|
249
|
+
\x1B[1mOPTIONS\x1B[0m
|
|
250
|
+
-s, --secret <secret> Webhook signing secret (whsec_...) [or QUASAR_WEBHOOK_SECRET]
|
|
251
|
+
-f, --forward-to <url> Local endpoint to forward to (default: ${DEFAULT_FORWARD_URL})
|
|
252
|
+
-a, --api-url <url> Quasar Cloud API base URL (default: ${DEFAULT_API_URL})
|
|
253
|
+
-e, --env-file <path> Custom path to .env file
|
|
254
|
+
-v, --version Show version number
|
|
255
|
+
-h, --help Show help details
|
|
256
|
+
|
|
257
|
+
\x1B[1mZERO-CONFIG SETUP\x1B[0m
|
|
258
|
+
Add to your local .env or .env.local:
|
|
259
|
+
QUASAR_WEBHOOK_SECRET=whsec_...
|
|
260
|
+
NEXT_PUBLIC_QUASAR_BASE_URL=https://api.tuwa.io # (optional)
|
|
261
|
+
|
|
262
|
+
Then simply run:
|
|
263
|
+
$ npx @tuwaio/quasar-sdk listen
|
|
264
|
+
`);
|
|
265
|
+
}
|
|
266
|
+
async function runListen(options) {
|
|
267
|
+
const { secret, forwardTo, apiUrl } = options;
|
|
268
|
+
const cleanApiUrl = apiUrl.replace(/\/+$/, "");
|
|
269
|
+
const listenUrl = `${cleanApiUrl}/v1/engine/webhooks/listen?secret=${encodeURIComponent(secret)}`;
|
|
270
|
+
console.log(`
|
|
271
|
+
\x1B[36m\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\x1B[0m
|
|
272
|
+
\x1B[36m\u2502\x1B[0m \x1B[1m\x1B[36m\u26A1 Quasar Webhook Local Dev Relay\x1B[0m \x1B[36m\u2502\x1B[0m
|
|
273
|
+
\x1B[36m\u2502\x1B[0m Forwarding to: \x1B[32m${forwardTo.padEnd(44)}\x1B[0m\x1B[36m\u2502\x1B[0m
|
|
274
|
+
\x1B[36m\u2502\x1B[0m Quasar Cloud: \x1B[34m${cleanApiUrl.padEnd(44)}\x1B[0m\x1B[36m\u2502\x1B[0m
|
|
275
|
+
\x1B[36m\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\x1B[0m
|
|
276
|
+
`);
|
|
277
|
+
let reconnectDelay = 1e3;
|
|
278
|
+
let isShuttingDown = false;
|
|
279
|
+
let activeAbortController = null;
|
|
280
|
+
const cleanup = () => {
|
|
281
|
+
isShuttingDown = true;
|
|
282
|
+
if (activeAbortController) {
|
|
283
|
+
activeAbortController.abort();
|
|
284
|
+
}
|
|
285
|
+
console.log("\n\x1B[33m[relay]\x1B[0m Disconnected. Bye!");
|
|
286
|
+
process.exit(0);
|
|
287
|
+
};
|
|
288
|
+
process.on("SIGINT", cleanup);
|
|
289
|
+
process.on("SIGTERM", cleanup);
|
|
290
|
+
while (!isShuttingDown) {
|
|
291
|
+
try {
|
|
292
|
+
activeAbortController = new AbortController();
|
|
293
|
+
console.log(`\x1B[90mConnecting to Quasar Cloud SSE stream...\x1B[0m`);
|
|
294
|
+
const response = await fetch(listenUrl, {
|
|
295
|
+
method: "GET",
|
|
296
|
+
headers: {
|
|
297
|
+
Accept: "text/event-stream",
|
|
298
|
+
"Cache-Control": "no-cache"
|
|
299
|
+
},
|
|
300
|
+
signal: activeAbortController.signal
|
|
301
|
+
});
|
|
302
|
+
if (!response.ok) {
|
|
303
|
+
if (response.status === 401) {
|
|
304
|
+
console.error(
|
|
305
|
+
`\x1B[31m\u2716 Unauthorized (401):\x1B[0m Invalid webhook signing secret. Check your QUASAR_WEBHOOK_SECRET.`
|
|
306
|
+
);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
if (response.status === 404) {
|
|
310
|
+
console.error(
|
|
311
|
+
`\x1B[31m\u2716 Endpoint Not Found (404):\x1B[0m No webhook endpoint found matching this signing secret.`
|
|
312
|
+
);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
const errText = await response.text();
|
|
316
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}: ${errText}`);
|
|
317
|
+
}
|
|
318
|
+
if (!response.body) {
|
|
319
|
+
throw new Error("No readable stream returned by server.");
|
|
320
|
+
}
|
|
321
|
+
console.log(`\x1B[32m\u2714 Connected!\x1B[0m Ready and listening for events...`);
|
|
322
|
+
reconnectDelay = 1e3;
|
|
323
|
+
const reader = response.body.getReader();
|
|
324
|
+
const decoder = new TextDecoder("utf-8");
|
|
325
|
+
let buffer = "";
|
|
326
|
+
while (!isShuttingDown) {
|
|
327
|
+
const { done, value } = await reader.read();
|
|
328
|
+
if (done) break;
|
|
329
|
+
buffer += decoder.decode(value, { stream: true });
|
|
330
|
+
const lines = buffer.split("\n");
|
|
331
|
+
buffer = lines.pop() || "";
|
|
332
|
+
let currentEvent = "message";
|
|
333
|
+
let currentData = "";
|
|
334
|
+
for (const line of lines) {
|
|
335
|
+
const trimmed = line.trim();
|
|
336
|
+
if (!trimmed) {
|
|
337
|
+
if (currentData) {
|
|
338
|
+
await handleSseMessage(currentEvent, currentData, forwardTo);
|
|
339
|
+
currentEvent = "message";
|
|
340
|
+
currentData = "";
|
|
341
|
+
}
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (trimmed.startsWith("event:")) {
|
|
345
|
+
currentEvent = trimmed.slice(6).trim();
|
|
346
|
+
} else if (trimmed.startsWith("data:")) {
|
|
347
|
+
const dataLine = trimmed.slice(5).trim();
|
|
348
|
+
currentData = currentData ? `${currentData}
|
|
349
|
+
${dataLine}` : dataLine;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} catch (err) {
|
|
354
|
+
if (isShuttingDown) break;
|
|
355
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
356
|
+
console.warn(`\x1B[33m[warn]\x1B[0m Connection lost (${message}). Reconnecting in ${reconnectDelay}ms...`);
|
|
357
|
+
await new Promise((resolve) => setTimeout(resolve, reconnectDelay));
|
|
358
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 15e3);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async function handleSseMessage(event, rawData, forwardTo) {
|
|
363
|
+
if (event === "ping" || rawData === ":keepalive" || rawData.startsWith(":")) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
const payload = JSON.parse(rawData);
|
|
368
|
+
if (event === "connected") {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (event === "payload" || event === "webhook" || event === "message") {
|
|
372
|
+
const dataObj = payload;
|
|
373
|
+
const deliveryId = dataObj.deliveryId || dataObj.id || "";
|
|
374
|
+
const eventType = dataObj.event || dataObj.eventType || dataObj.action || "webhook";
|
|
375
|
+
const forwardedData = dataObj.data !== void 0 ? dataObj.data : dataObj.payload !== void 0 ? dataObj.payload : dataObj;
|
|
376
|
+
const signature = dataObj.signature || "";
|
|
377
|
+
const startTime = Date.now();
|
|
378
|
+
const timeStr = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
379
|
+
try {
|
|
380
|
+
const res = await fetch(forwardTo, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: {
|
|
383
|
+
"Content-Type": "application/json",
|
|
384
|
+
"x-quasar-signature": signature,
|
|
385
|
+
"x-quasar-event": eventType,
|
|
386
|
+
"x-quasar-delivery-id": deliveryId
|
|
387
|
+
},
|
|
388
|
+
body: JSON.stringify(forwardedData)
|
|
389
|
+
});
|
|
390
|
+
const duration = Date.now() - startTime;
|
|
391
|
+
if (res.ok) {
|
|
392
|
+
console.log(
|
|
393
|
+
`\x1B[90m${timeStr}\x1B[0m \x1B[32m[${res.status} OK]\x1B[0m \x1B[1m${eventType}\x1B[0m \u2794 ${forwardTo} \x1B[90m(${duration}ms)\x1B[0m`
|
|
394
|
+
);
|
|
395
|
+
} else {
|
|
396
|
+
console.warn(
|
|
397
|
+
`\x1B[90m${timeStr}\x1B[0m \x1B[31m[${res.status} ${res.statusText}]\x1B[0m \x1B[1m${eventType}\x1B[0m \u2794 ${forwardTo} \x1B[90m(${duration}ms)\x1B[0m`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
} catch (postErr) {
|
|
401
|
+
const errMsg = postErr instanceof Error ? postErr.message : String(postErr);
|
|
402
|
+
console.error(
|
|
403
|
+
`\x1B[90m${timeStr}\x1B[0m \x1B[31m[FAIL]\x1B[0m \x1B[1m${eventType}\x1B[0m \u2794 Could not reach ${forwardTo}: ${errMsg}`
|
|
404
|
+
);
|
|
405
|
+
console.error(` \x1B[33mTip: Make sure your local application is running on ${forwardTo}\x1B[0m`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async function main() {
|
|
412
|
+
const rawArgs = process.argv.slice(2);
|
|
413
|
+
const options = parseArgs(rawArgs);
|
|
414
|
+
if (options.help || rawArgs.length === 0 && !options.version) {
|
|
415
|
+
printHelp();
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (options.version) {
|
|
419
|
+
console.log(`@tuwaio/quasar-sdk v${CLI_VERSION}`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (options.command !== "listen") {
|
|
423
|
+
console.error(`\x1B[31m\u2716 Unknown command:\x1B[0m "${options.command}". Did you mean "listen"?`);
|
|
424
|
+
console.log(`Run \x1B[1mnpx @tuwaio/quasar-sdk --help\x1B[0m for available commands.`);
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
const env = loadEnv(options.envFile);
|
|
428
|
+
const secret = options.secret || process.env.QUASAR_WEBHOOK_SECRET || env.QUASAR_WEBHOOK_SECRET;
|
|
429
|
+
const forwardTo = options.forwardTo || process.env.QUASAR_WEBHOOK_FORWARD_TO || env.QUASAR_WEBHOOK_FORWARD_TO || DEFAULT_FORWARD_URL;
|
|
430
|
+
const apiUrl = options.apiUrl || process.env.NEXT_PUBLIC_QUASAR_BASE_URL || env.NEXT_PUBLIC_QUASAR_BASE_URL || process.env.QUASAR_BASE_URL || env.QUASAR_BASE_URL || DEFAULT_API_URL;
|
|
431
|
+
if (!secret) {
|
|
432
|
+
console.error(`
|
|
433
|
+
\x1B[31m\u2716 Missing Webhook Signing Secret\x1B[0m
|
|
434
|
+
The local relay requires your webhook endpoint signing secret (starting with whsec_).
|
|
435
|
+
|
|
436
|
+
\x1B[1mHow to fix:\x1B[0m
|
|
437
|
+
1. Provide via command-line argument:
|
|
438
|
+
$ npx @tuwaio/quasar-sdk listen --secret whsec_...
|
|
439
|
+
|
|
440
|
+
2. Or add to your local .env / .env.local file:
|
|
441
|
+
QUASAR_WEBHOOK_SECRET=whsec_...
|
|
442
|
+
`);
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
await runListen({
|
|
446
|
+
secret,
|
|
447
|
+
forwardTo,
|
|
448
|
+
apiUrl
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
if (!process.env.VITEST) {
|
|
452
|
+
void main().catch((err) => {
|
|
453
|
+
console.error("\x1B[31mFatal error:\x1B[0m", err);
|
|
454
|
+
process.exit(1);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
458
|
+
0 && (module.exports = {
|
|
459
|
+
CLI_VERSION,
|
|
460
|
+
DEFAULT_API_URL,
|
|
461
|
+
DEFAULT_FORWARD_URL,
|
|
462
|
+
handleSseMessage,
|
|
463
|
+
loadEnv,
|
|
464
|
+
main,
|
|
465
|
+
parseArgs,
|
|
466
|
+
parseEnvFile
|
|
467
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuwaio/quasar-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"author": "Oleksandr Tkach",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"dist",
|
|
24
24
|
"socket.json"
|
|
25
25
|
],
|
|
26
|
+
"bin": {
|
|
27
|
+
"@tuwaio/quasar-sdk": "./dist/cli.cjs",
|
|
28
|
+
"quasar-sdk": "./dist/cli.cjs"
|
|
29
|
+
},
|
|
26
30
|
"keywords": [
|
|
27
31
|
"tuwa",
|
|
28
32
|
"web3",
|