Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ name: Release Desktop
# - Push of a `desktop-v*` tag builds all platforms.
#
# Required repo secrets:
# Telegram feedback:
# ANTSEED_FEEDBACK_TELEGRAM_BOT_TOKEN dedicated feedback bot token
# ANTSEED_FEEDBACK_TELEGRAM_CHAT_ID numeric channel ID or @username
# macOS:
# MAC_CSC_LINK base64 of a .p12 containing the Developer
# ID Application cert + private key
Expand Down Expand Up @@ -124,6 +127,12 @@ jobs:
ANTSEED_COMPARABLE_PRICES_URL: ${{ vars.ANTSEED_COMPARABLE_PRICES_URL }}
run: node scripts/bake-comparable-prices-url.mjs --require

- name: Bake Telegram feedback configuration
env:
ANTSEED_FEEDBACK_TELEGRAM_BOT_TOKEN: ${{ secrets.ANTSEED_FEEDBACK_TELEGRAM_BOT_TOKEN }}
ANTSEED_FEEDBACK_TELEGRAM_CHAT_ID: ${{ secrets.ANTSEED_FEEDBACK_TELEGRAM_CHAT_ID }}
run: node scripts/bake-feedback-telegram-config.mjs --require

- name: Build monorepo (tiers 0-3)
run: |
pnpm run build:tier0
Expand Down Expand Up @@ -221,6 +230,13 @@ jobs:
run: node scripts/bake-comparable-prices-url.mjs --require
shell: pwsh

- name: Bake Telegram feedback configuration
env:
ANTSEED_FEEDBACK_TELEGRAM_BOT_TOKEN: ${{ secrets.ANTSEED_FEEDBACK_TELEGRAM_BOT_TOKEN }}
ANTSEED_FEEDBACK_TELEGRAM_CHAT_ID: ${{ secrets.ANTSEED_FEEDBACK_TELEGRAM_CHAT_ID }}
run: node scripts/bake-feedback-telegram-config.mjs --require
shell: pwsh

- name: Build monorepo (tiers 0-3)
run: |
pnpm run build:tier0
Expand Down Expand Up @@ -340,6 +356,12 @@ jobs:
ANTSEED_COMPARABLE_PRICES_URL: ${{ vars.ANTSEED_COMPARABLE_PRICES_URL }}
run: node scripts/bake-comparable-prices-url.mjs --require

- name: Bake Telegram feedback configuration
env:
ANTSEED_FEEDBACK_TELEGRAM_BOT_TOKEN: ${{ secrets.ANTSEED_FEEDBACK_TELEGRAM_BOT_TOKEN }}
ANTSEED_FEEDBACK_TELEGRAM_CHAT_ID: ${{ secrets.ANTSEED_FEEDBACK_TELEGRAM_CHAT_ID }}
run: node scripts/bake-feedback-telegram-config.mjs --require

- name: Build monorepo (tiers 0-3)
run: |
pnpm run build:tier0
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ This project uses selective package publishing. Each release entry lists the pub

### Added

- Desktop now includes an Emdash-inspired feedback modal in Help & Support, with optional contact email, image attachments, and opt-in privacy-redacted diagnostic logs delivered to the AntSeed Telegram feedback channel.

- Added `@antseed/web-sdk`, a browser buyer SDK: connects to unmodified sellers over WebRTC DataChannels (signaled through a relay) and runs the shared `@antseed/buyer-core` request/payment stack — 402 negotiation, in-browser EIP-712 ReserveAuth/SpendingAuth signing, SSE streaming, and chunked uploads. `AntseedWebClient.create()` durably commits complete channel recovery state to IndexedDB before transmitting authorizations, surfaces background storage failures through `onPersistenceError`, and uses an identity-scoped Web Lock to prevent concurrent signing from multiple tabs; the public constructor and explicit `ephemeral()` mode retain injectable/in-memory operation for compatibility, tests, and free interoperability experiments. The `BuyerChannelStore` contract now supports an atomic authorization commit, an optional async `flush()` durability barrier, and lifecycle cleanup; the buyer stack persists and recovers ReserveAuth/SpendingAuth signatures, reserve/top-up state, encoded metadata, and per-service usage. Browser uploads chunk above 192 KiB to stay under the ~256 KiB SCTP message ceiling (`ProxyMux` gained an `uploadThresholdBytes` option). The client takes any address-bearing ethers `AbstractSigner` (`BuyerSigner`) instead of requiring a concrete `Wallet`, supports full `RTCIceServer` entries (TURN credentials) plus `iceTransportPolicy`, reports the selected WebRTC path (`direct`/`relay`/`unknown`) via `onConnectionInfo` without exposing addresses, and ships a working browser example page (`packages/web-sdk/examples/example.html`, served by `pnpm --filter @antseed/web-sdk run example`).
- Added `@antseed/relay`, the web relay browser buyers need: a DHT-discovered seller snapshot at `GET /sellers` and a `WS /bridge/<peerId>` byte pipe to the seller's TCP signaling port. The bridge dials only cached seller endpoints, refuses private/internal address ranges for DHT-announced sellers (checked for IP literals and again at DNS resolution), enforces per-IP, per-seller, and global bridge caps plus a configurable WebSocket message-size limit, supports an optional browser Origin allowlist, exposes cache-aware readiness and privacy-safe aggregate metrics, and supports `RELAY_TRUST_PROXY=1` for deployments behind a TLS-terminating proxy.
- CLI: `antseed buyer deposit` is now the deposit flow — it prints the node's funding address and a terminal QR code (EIP-681 payment request), serves the browser-wallet checkout page and prints its link for users who prefer depositing from a connected wallet, then watches and deposits incoming USDC into the buyer's credits automatically via the gasless relayer sweep. Works through a running buyer daemon or standalone with an ephemeral node; `--amount` prefills the QR request and checkout, `--no-watch` prints without waiting, and `antseed deposit` works as an alias. The previous `buyer deposit <amount>` direct on-chain deposit (hot wallet pays gas) moved to `buyer deposit --onchain <usdc>`.
Expand Down
27 changes: 27 additions & 0 deletions apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,33 @@ Build desktop assets:
npm run build
```

### Telegram feedback channel

The Help & Support feedback action posts through a dedicated Telegram bot. Source builds
keep the action disabled unless both variables are configured:

```bash
export ANTSEED_FEEDBACK_TELEGRAM_BOT_TOKEN='123456:bot-token'
export ANTSEED_FEEDBACK_TELEGRAM_CHAT_ID='@channelusername' # or -100…
npm run dev
```

Release CI runs `scripts/bake-feedback-telegram-config.mjs --require` before
compilation, using GitHub Actions secrets with the same names. The generated
source defaults remain `null` in git; the runtime environment overrides baked
values for local testing.

Use a disposable bot that is an administrator only in the feedback channel and
grant only **Post Messages**. Never reuse the personal Telegram bridge bot. The
token is compiled into public Electron artifacts and can be extracted, so treat
it as exposed: monitor the channel, remove the bot or revoke the token to disable
submissions, and rotate it immediately if abused. A server-side relay is required
before this channel handles sensitive or high-volume production feedback.

Diagnostic logs are opt-in and privacy-redacted before upload. The app masks
credentials, emails, local home paths, IP addresses, wallet addresses, and peer
IDs, retains at most 500 recent entries, and caps the log at 512 KiB.

Start app from built assets:

```bash
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"type": "module",
"main": "dist/main/main.js",
"scripts": {
"ensure:cli-dist": "pnpm -C ../../packages/api-adapter build && pnpm -C ../../packages/protocol build && pnpm -C ../../packages/buyer-core build && pnpm -C ../../packages/router-core build && pnpm -C ../../plugins/router-local build && pnpm -C ../../packages/node build && pnpm -C ../payments build:server && pnpm -C ../cli build",
"ensure:cli-dist": "pnpm -C ../../packages/api-adapter build && pnpm -C ../../packages/protocol build && pnpm -C ../../packages/buyer-core build && pnpm -C ../../packages/node build && pnpm -C ../../packages/provider-core build && pnpm -C ../../packages/ant-agent build && pnpm -C ../../packages/router-core build && pnpm -C ../../plugins/router-local build && pnpm -C ../payments build:server && pnpm -C ../cli build",
"brand:electron-dev": "node ./scripts/brand-electron-dev.mjs",
"models:update-metadata": "pnpm -C ../../packages/node build && node ./scripts/update-model-metadata.mjs",
"ensure:native": "npm run ensure:runtime-native",
Expand Down
48 changes: 48 additions & 0 deletions apps/desktop/scripts/bake-feedback-telegram-config.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
import { test } from 'node:test';

const repoRoot = resolve(import.meta.dirname, '..', '..', '..');
const script = join(repoRoot, 'scripts', 'bake-feedback-telegram-config.mjs');

test('bakes Telegram feedback credentials without printing the token', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'antseed-feedback-bake-'));
const target = join(tempDir, 'baked-defaults.ts');
const token = ['123456789', 'abcdefghijklmnopqrstuvwxyz_ABCDEF'].join(':');
writeFileSync(target, [
'export const BAKED_FEEDBACK_TELEGRAM_BOT_TOKEN: string | null = null;',
'export const BAKED_FEEDBACK_TELEGRAM_CHAT_ID: string | null = null;',
'',
].join('\n'));

const result = spawnSync(process.execPath, [script, '--require', '--target', target], {
encoding: 'utf8',
env: {
...process.env,
ANTSEED_FEEDBACK_TELEGRAM_BOT_TOKEN: token,
ANTSEED_FEEDBACK_TELEGRAM_CHAT_ID: '@antseed_feedback',
},
});

assert.equal(result.status, 0, result.stderr);
assert.doesNotMatch(`${result.stdout}${result.stderr}`, new RegExp(token));
const baked = readFileSync(target, 'utf8');
assert.match(baked, new RegExp(token));
assert.match(baked, /@antseed_feedback/);
});

test('required mode rejects incomplete configuration', () => {
const result = spawnSync(process.execPath, [script, '--require'], {
encoding: 'utf8',
env: {
...process.env,
ANTSEED_FEEDBACK_TELEGRAM_BOT_TOKEN: ['123456789', 'abcdefghijklmnopqrstuvwxyz_ABCDEF'].join(':'),
ANTSEED_FEEDBACK_TELEGRAM_CHAT_ID: '',
},
});
assert.notEqual(result.status, 0);
assert.match(result.stderr, /must both be set/);
});
69 changes: 69 additions & 0 deletions apps/desktop/src/main/feedback/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
parseTelegramChatId,
resolveFeedbackTelegramConfig,
} from './config.js';

const validToken = ['123456789', 'abcdefghijklmnopqrstuvwxyz_ABCDEF'].join(':');

test('reports missing source-build configuration', () => {
assert.deepEqual(resolveFeedbackTelegramConfig({}, { botToken: null, chatId: null }), {
configured: false,
error: 'Feedback is unavailable in this build.',
});
});

test('resolves a valid runtime Telegram feedback configuration', () => {
const result = resolveFeedbackTelegramConfig({
ANTSEED_FEEDBACK_TELEGRAM_BOT_TOKEN: validToken,
ANTSEED_FEEDBACK_TELEGRAM_CHAT_ID: '@antseed_feedback',
});
assert.equal(result.configured, true);
if (result.configured) {
assert.equal(result.config.botToken, validToken);
assert.equal(result.config.chatId, '@antseed_feedback');
}
});

test('uses baked configuration unless runtime values override it', () => {
const baked = { botToken: validToken, chatId: '@baked_feedback' };
const bakedResult = resolveFeedbackTelegramConfig({}, baked);
assert.equal(bakedResult.configured, true);
if (bakedResult.configured) assert.equal(bakedResult.config.chatId, '@baked_feedback');

const runtimeResult = resolveFeedbackTelegramConfig({
ANTSEED_FEEDBACK_TELEGRAM_BOT_TOKEN: validToken,
ANTSEED_FEEDBACK_TELEGRAM_CHAT_ID: '@runtime_feedback',
}, baked);
assert.equal(runtimeResult.configured, true);
if (runtimeResult.configured) assert.equal(runtimeResult.config.chatId, '@runtime_feedback');
});

test('runtime override requires both values and can disable baked defaults', () => {
const baked = { botToken: validToken, chatId: '@baked_feedback' };
const incomplete = resolveFeedbackTelegramConfig({
ANTSEED_FEEDBACK_TELEGRAM_BOT_TOKEN: validToken,
}, baked);
assert.equal(incomplete.configured, false);

const disabled = resolveFeedbackTelegramConfig({
ANTSEED_FEEDBACK_TELEGRAM_BOT_TOKEN: '',
ANTSEED_FEEDBACK_TELEGRAM_CHAT_ID: '',
}, baked);
assert.deepEqual(disabled, { configured: false, error: 'Feedback is unavailable in this build.' });
});

test('rejects invalid tokens and parses numeric and public-channel chat identifiers', () => {
const invalidToken = resolveFeedbackTelegramConfig({
ANTSEED_FEEDBACK_TELEGRAM_BOT_TOKEN: 'not-a-token',
ANTSEED_FEEDBACK_TELEGRAM_CHAT_ID: '@antseed_feedback',
});
assert.deepEqual(invalidToken, {
configured: false,
error: 'Telegram feedback bot configuration is invalid.',
});
assert.equal(parseTelegramChatId('-1001234567890'), -1001234567890);
assert.equal(parseTelegramChatId('@antseed_feedback'), '@antseed_feedback');
assert.equal(parseTelegramChatId('not a chat'), null);
});
74 changes: 74 additions & 0 deletions apps/desktop/src/main/feedback/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
BAKED_FEEDBACK_TELEGRAM_BOT_TOKEN,
BAKED_FEEDBACK_TELEGRAM_CHAT_ID,
} from '../generated/baked-defaults.js';

export const FEEDBACK_TELEGRAM_BOT_TOKEN_ENV = 'ANTSEED_FEEDBACK_TELEGRAM_BOT_TOKEN';
export const FEEDBACK_TELEGRAM_CHAT_ID_ENV = 'ANTSEED_FEEDBACK_TELEGRAM_CHAT_ID';

export type FeedbackTelegramConfig = {
botToken: string;
chatId: number | string;
};

export type FeedbackTelegramConfigResolution =
| { configured: true; config: FeedbackTelegramConfig }
| { configured: false; error: string };

type FeedbackTelegramBakedDefaults = {
botToken: string | null;
chatId: string | null;
};

export function isValidTelegramBotToken(value: string): boolean {
return /^\d+:[A-Za-z0-9_-]{20,}$/.test(value);
}

export function parseTelegramChatId(value: string): number | string | null {
const trimmed = value.trim();
if (/^-?\d+$/.test(trimmed)) {
const parsed = Number(trimmed);
return Number.isSafeInteger(parsed) ? parsed : null;
}
if (/^@[A-Za-z][A-Za-z0-9_]{4,31}$/.test(trimmed)) {
return trimmed;
}
return null;
}

export function resolveFeedbackTelegramConfig(
env: NodeJS.ProcessEnv = process.env,
baked: FeedbackTelegramBakedDefaults = {
botToken: BAKED_FEEDBACK_TELEGRAM_BOT_TOKEN,
chatId: BAKED_FEEDBACK_TELEGRAM_CHAT_ID,
},
): FeedbackTelegramConfigResolution {
const hasRuntimeOverride =
Object.prototype.hasOwnProperty.call(env, FEEDBACK_TELEGRAM_BOT_TOKEN_ENV)
|| Object.prototype.hasOwnProperty.call(env, FEEDBACK_TELEGRAM_CHAT_ID_ENV);
const botToken = (
hasRuntimeOverride
? env[FEEDBACK_TELEGRAM_BOT_TOKEN_ENV]
: baked.botToken
)?.trim() ?? '';
const rawChatId = (
hasRuntimeOverride
? env[FEEDBACK_TELEGRAM_CHAT_ID_ENV]
: baked.chatId
)?.trim() ?? '';

if (!botToken && !rawChatId) {
return { configured: false, error: 'Feedback is unavailable in this build.' };
}
if (!botToken || !rawChatId) {
return { configured: false, error: 'Telegram feedback configuration is incomplete.' };
}
if (!isValidTelegramBotToken(botToken)) {
return { configured: false, error: 'Telegram feedback bot configuration is invalid.' };
}
const chatId = parseTelegramChatId(rawChatId);
if (chatId === null) {
return { configured: false, error: 'Telegram feedback channel configuration is invalid.' };
}
return { configured: true, config: { botToken, chatId } };
}
52 changes: 52 additions & 0 deletions apps/desktop/src/main/feedback/diagnostic-log.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import type { LogEvent } from '../runtime/log-parser.js';
import { buildFeedbackDiagnosticLog } from './diagnostic-log.js';

function log(line: string, timestamp = 1_700_000_000_000): LogEvent {
return { mode: 'connect', stream: 'system', line, timestamp };
}

test('privacy-redacts sensitive diagnostic values with stable placeholders', () => {
const peerId = 'abcdefabcdefabcdefabcdefabcdefabcdefabcd';
const botToken = ['123456789', 'abcdefghijklmnopqrstuvwxyz_ABCDEF'].join(':');
const webhook = ['https://discord.com/api/webhooks', '123456789', 'webhook-secret-value'].join('/');
const output = new TextDecoder().decode(buildFeedbackDiagnosticLog([
log(`token=${botToken} authorization=Basic-sensitive webhook=${webhook} email=user@example.com`),
log(`home=/Users/alex/project ip=192.168.1.10 wallet=0x${'a'.repeat(40)} peer=${peerId}`),
log('again user@example.com and 192.168.1.10'),
]));

assert.doesNotMatch(output, /abcdefghijklmnopqrstuvwxyz_ABCDEF/);
assert.doesNotMatch(output, /user@example\.com/);
assert.doesNotMatch(output, /webhook-secret-value|Basic-sensitive/);
assert.doesNotMatch(output, /\/Users\/alex/);
assert.doesNotMatch(output, /192\.168\.1\.10/);
assert.doesNotMatch(output, new RegExp(peerId));
assert.equal(output.match(/<EMAIL_1>/g)?.length, 2);
assert.equal(output.match(/<IP_1>/g)?.length, 2);
assert.match(output, /<WALLET_1>/);
assert.match(output, /<PEER_1>/);
assert.match(output, /<WEBHOOK_1>/);
assert.match(output, /<SECRET_2>/);
});

test('retains only the newest 500 runtime entries', () => {
const logs = Array.from({ length: 501 }, (_value, index) => log(`entry-${index}`, index));
const output = new TextDecoder().decode(buildFeedbackDiagnosticLog(logs));
assert.doesNotMatch(output, /entry-0(?:\D|$)/);
assert.match(output, /entry-1(?:\D|$)/);
assert.match(output, /entry-500(?:\D|$)/);
});

test('keeps the newest entries within the byte budget', () => {
const output = new TextDecoder().decode(buildFeedbackDiagnosticLog([
log(`old-${'x'.repeat(100)}`, 1),
log(`middle-${'y'.repeat(100)}`, 2),
log('newest-entry', 3),
], 260));
assert.ok(Buffer.byteLength(output, 'utf8') <= 260);
assert.match(output, /newest-entry/);
assert.doesNotMatch(output, /old-/);
assert.match(output, /earlier log entries omitted/);
});
Loading
Loading