pi-harness-runtime 1.1.387 → 1.1.388

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 (127) hide show
  1. package/AGENTS.md +9 -4
  2. package/index.ts +20 -18
  3. package/package.json +1 -1
  4. package/packages/a2a-adapter/package.json +1 -1
  5. package/packages/agent-policy/package.json +1 -1
  6. package/packages/architecture-generator/package.json +1 -1
  7. package/packages/auth/package.json +1 -1
  8. package/packages/autonomous-refactor/package.json +1 -1
  9. package/packages/autonomous-runtime/package.json +1 -1
  10. package/packages/cache-strategy/package.json +1 -1
  11. package/packages/capability-registry/package.json +1 -1
  12. package/packages/checkpoint/package.json +1 -1
  13. package/packages/cli-plugin-sdk/package.json +1 -1
  14. package/packages/clipboard/package.json +1 -1
  15. package/packages/clipboard-plugin/package.json +1 -1
  16. package/packages/code-generation/package.json +1 -1
  17. package/packages/code-review/package.json +1 -1
  18. package/packages/codex-adapter/package.json +1 -1
  19. package/packages/codex-watcher/package.json +1 -1
  20. package/packages/config-capture/package.json +1 -1
  21. package/packages/connection-runtime/package.json +1 -1
  22. package/packages/context-compiler/package.json +1 -1
  23. package/packages/context-discovery/package.json +1 -1
  24. package/packages/context-manager/package.json +1 -1
  25. package/packages/contracts/dist/workspace-env-format.d.ts +71 -0
  26. package/packages/contracts/dist/workspace-env-format.js +120 -0
  27. package/packages/contracts/dist/workspace-env.d.ts +7 -30
  28. package/packages/contracts/dist/workspace-env.js +18 -57
  29. package/packages/contracts/package.json +1 -1
  30. package/packages/contracts/src/workspace-env-format.ts +165 -0
  31. package/packages/contracts/src/workspace-env.ts +27 -99
  32. package/packages/contracts/test/workspace-env.test.ts +144 -0
  33. package/packages/cookie-sanitizer/package.json +1 -1
  34. package/packages/cost-optimizer/package.json +1 -1
  35. package/packages/dependency-analyzer/package.json +1 -1
  36. package/packages/django-plugin/package.json +1 -1
  37. package/packages/doc-generator/package.json +1 -1
  38. package/packages/durable-review/package.json +1 -1
  39. package/packages/e2e-tester/package.json +1 -1
  40. package/packages/evaluation-engine/package.json +1 -1
  41. package/packages/evaluation-runner/package.json +1 -1
  42. package/packages/event-bus/package.json +1 -1
  43. package/packages/event-sink/package.json +1 -1
  44. package/packages/event-store/package.json +1 -1
  45. package/packages/experience-replay/package.json +1 -1
  46. package/packages/feedback-collector/package.json +1 -1
  47. package/packages/file-copy-helper/package.json +1 -1
  48. package/packages/framework-detector/package.json +1 -1
  49. package/packages/framework-plugin-sdk/package.json +1 -1
  50. package/packages/frappe-plugin/package.json +1 -1
  51. package/packages/generic-web-plugin/package.json +1 -1
  52. package/packages/health-monitor/package.json +1 -1
  53. package/packages/intent-analyzer/package.json +1 -1
  54. package/packages/jev-judge/package.json +1 -1
  55. package/packages/knowledge-graph/package.json +1 -1
  56. package/packages/knowledge-retrieval/package.json +1 -1
  57. package/packages/laravel-plugin/package.json +1 -1
  58. package/packages/learning-engine/package.json +1 -1
  59. package/packages/mcp-adapter/package.json +1 -1
  60. package/packages/memory-engine/package.json +1 -1
  61. package/packages/milestone-manager/package.json +1 -1
  62. package/packages/model-registry/package.json +1 -1
  63. package/packages/nextjs-plugin/package.json +1 -1
  64. package/packages/notification/package.json +1 -1
  65. package/packages/observability/package.json +1 -1
  66. package/packages/okf-indexer/package.json +1 -1
  67. package/packages/paperclip-integration/package.json +1 -1
  68. package/packages/performance-optimizer/package.json +1 -1
  69. package/packages/permission-mode/package.json +1 -1
  70. package/packages/privilege-broker/package.json +1 -1
  71. package/packages/project-analyzer/package.json +1 -1
  72. package/packages/project-bootstrap/package.json +1 -1
  73. package/packages/projection-engine/package.json +1 -1
  74. package/packages/prompt-compiler/package.json +1 -1
  75. package/packages/prompt-versioning/package.json +1 -1
  76. package/packages/provider-adapter-sdk/package.json +1 -1
  77. package/packages/provider-router/package.json +1 -1
  78. package/packages/provider-selector/package.json +1 -1
  79. package/packages/providers/package.json +1 -1
  80. package/packages/qdrant-skills/package.json +1 -1
  81. package/packages/quota-manager/package.json +1 -1
  82. package/packages/rate-limiter/package.json +1 -1
  83. package/packages/react-vite-plugin/package.json +1 -1
  84. package/packages/relay/dist/index.d.ts +3 -0
  85. package/packages/relay/dist/index.js +3 -0
  86. package/packages/relay/dist/relay-endpoint.d.ts +94 -0
  87. package/packages/relay/dist/relay-endpoint.js +239 -0
  88. package/packages/relay/package.json +1 -1
  89. package/packages/relay/src/index.ts +17 -0
  90. package/packages/relay/src/relay-endpoint.ts +304 -0
  91. package/packages/relay/test/relay-endpoint.test.ts +278 -0
  92. package/packages/release-manager/package.json +1 -1
  93. package/packages/requirement-compiler/package.json +1 -1
  94. package/packages/runtime/package.json +1 -1
  95. package/packages/scheduler/package.json +1 -1
  96. package/packages/scheduler-adapter/package.json +1 -1
  97. package/packages/session/package.json +1 -1
  98. package/packages/session-api/package.json +1 -1
  99. package/packages/session-export/package.json +1 -1
  100. package/packages/shared-context/package.json +1 -1
  101. package/packages/skill-mcp-client/package.json +1 -1
  102. package/packages/skill-registry/package.json +1 -1
  103. package/packages/skills/package.json +1 -1
  104. package/packages/sprint-planner/package.json +1 -1
  105. package/packages/subscription-engine/package.json +1 -1
  106. package/packages/task-compiler/package.json +1 -1
  107. package/packages/telegram/package.json +1 -1
  108. package/packages/tencentdb-memory/package.json +1 -1
  109. package/packages/tencentdb-sync/package.json +1 -1
  110. package/packages/test-data-generator/package.json +1 -1
  111. package/packages/test-generator/package.json +1 -1
  112. package/packages/todo-bd-sync/package.json +1 -1
  113. package/packages/token-estimation/package.json +1 -1
  114. package/packages/token-optimizer/package.json +1 -1
  115. package/packages/trajectory/package.json +1 -1
  116. package/packages/tui/package.json +1 -1
  117. package/packages/tui/test/harness-footer.test.ts +1 -1
  118. package/packages/types/package.json +1 -1
  119. package/packages/voice-api/package.json +1 -1
  120. package/packages/webmcp-tester/package.json +1 -1
  121. package/packages/wiki/package.json +1 -1
  122. package/packages/workflow-events/package.json +1 -1
  123. package/packages/workspace-scanner/package.json +1 -1
  124. package/packages/worktree/package.json +1 -1
  125. package/packages/write-review/package.json +1 -1
  126. package/scripts/relay-client.ts +24 -4
  127. package/scripts/unified-webhook-server.ts +29 -22
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/knowledge-graph",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Knowledge Graph extraction and provenance linking for SKILL.md files",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/knowledge-retrieval",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Knowledge retrieval via TencentDB-Agent-Memory MCP (RFC-0105)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/laravel-plugin",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Laravel framework analysis — artisan commands, Eloquent models, Blade",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/learning-engine",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Learning engine for extracting lessons from runtime execution (RFC-0058)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/mcp-adapter",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "type": "module",
5
5
  "description": "MCP server adapter for pi-harness — exposes harness capabilities as MCP tools/resources/prompts (RFC-0068)",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/memory-engine",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Memory engine with OKF integration for durable knowledge management (RFC-0060)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/milestone-manager",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Track milestones and alerts when items are at risk",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/model-registry",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Model Registry for pi-harness-runtime — centralized model metadata with pricing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/nextjs-plugin",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Next.js framework detection and analysis for pi-harness-runtime (RFC-0062)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/notification",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "pi-harness-runtime — notification",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/observability",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Comprehensive observability system with structured logging, tracing, metrics, and alerting",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/okf-indexer",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "OKF document indexer for SKILL.md files (RFC-0106)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/paperclip-integration",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Paperclip API integration: poll agent inbox, route tasks to pi.dev, Telegram alerts",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/performance-optimizer",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Performance optimization analyzer for pi-harness runtime (RFC-0056)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/permission-mode",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "type": "module",
5
5
  "description": "Runtime permission modes for pi-harness - Supervised/Auto/Full-access",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/privilege-broker",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Privilege Broker — capability registry and command executor for autonomous runtime (RFC-0101 §7)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/project-analyzer",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Project analysis system for pi-harness-runtime - detects frameworks, rules, and project structure",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/project-bootstrap",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Project scaffolding from typed ProjectSpec",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/projection-engine",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/prompt-compiler",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Deterministic prompt compiler for pi-harness-runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/prompt-versioning",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "type": "module",
5
5
  "description": "Version control for prompts with rollback support (RFC-0019)",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/provider-adapter-sdk",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "SDK for building, testing, and registering AI provider adapters",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/provider-router",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Enhanced Provider Router for pi-harness-runtime — intelligent routing with capabilities, costs, and quotas",
5
5
  "type": "module",
6
6
  "main": "./dist/provider-router.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/provider-selector",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Multi-Provider Selector — route requests by cost, quality, and latency (RFC-0012)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/providers",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "pi-harness-runtime — providers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/qdrant-skills",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "Qdrant vector search integration for Hermes-style skill system",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/quota-manager",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "pi-harness-runtime — quota-manager",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/rate-limiter",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "type": "module",
5
5
  "description": "Rate limiting with token bucket algorithm (RFC-0023)",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/react-vite-plugin",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "description": "React/Vite framework analysis — components, config, TypeScript setup",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,5 +12,8 @@ export { RelayServer, createRelayServer } from './server.js';
12
12
  export type { RelayServerOptions } from './server.js';
13
13
  export { RelayClient } from './client.js';
14
14
  export type { RelayClientCallbacks } from './client.js';
15
+ export { resolveRelayEndpoint, deriveRelayWsUrl, isRelayWsUrl, describeRelayEndpoint, } from './relay-endpoint.js';
16
+ export type { RelayEndpointEnv, RelayEndpointResolution, ResolveRelayEndpointOptions, } from './relay-endpoint.js';
17
+ export { RELAY_URL_SOURCES, RELAY_TOKEN_SOURCES, } from './relay-endpoint.js';
15
18
  export { PROTOCOL_VERSION, generateMessageId, createTimestamp, } from './types.js';
16
19
  export type { RelayConfig, RelayTunnel, ConnectionState, ConnectionStateChange, BaseMessage, SnapshotMessage, SynchronizedMessage, CallbackPayload, CallbackMessage, StatusKind, StatusMessage, PingMessage, PongMessage, AuthRequestMessage, AuthResponseMessage, SubscribeMessage, ConnectedMessage, ServerMessage, ClientMessage, RelayMessage, } from './types.js';
@@ -12,5 +12,8 @@
12
12
  export { RelayServer, createRelayServer } from './server.js';
13
13
  // Client
14
14
  export { RelayClient } from './client.js';
15
+ // Endpoint resolution (relay URL + auth token discovery)
16
+ export { resolveRelayEndpoint, deriveRelayWsUrl, isRelayWsUrl, describeRelayEndpoint, } from './relay-endpoint.js';
17
+ export { RELAY_URL_SOURCES, RELAY_TOKEN_SOURCES, } from './relay-endpoint.js';
15
18
  // Types
16
19
  export { PROTOCOL_VERSION, generateMessageId, createTimestamp, } from './types.js';
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Relay endpoint resolution.
3
+ *
4
+ * Resolves the relay WebSocket URL and the auth token from the SAME sources
5
+ * the relay server uses (see scripts/unified-webhook-server.ts), so a client
6
+ * and the server always agree:
7
+ *
8
+ * URL: RELAY_WS_URL env -> keys/relay-ws-url.txt -> derived from keys/telegram-webhook-url.txt
9
+ * Token: RELAY_AUTH_TOKEN env -> keys/relay-auth-token.txt -> keys/telegram-webhook-secret.txt
10
+ *
11
+ * The token value is never logged: only the NAME of the source it came from.
12
+ */
13
+ /** Environment variables consulted by the resolver. */
14
+ export interface RelayEndpointEnv {
15
+ RELAY_WS_URL?: string;
16
+ RELAY_AUTH_TOKEN?: string;
17
+ }
18
+ /** Result of a relay endpoint resolution. */
19
+ export interface RelayEndpointResolution {
20
+ /** WebSocket URL, or null when it could not be resolved. */
21
+ url: string | null;
22
+ /** Where the URL came from (e.g. "env RELAY_WS_URL"). Never contains a secret. */
23
+ urlSource: string | null;
24
+ /** Auth token, or null when it could not be resolved. */
25
+ token: string | null;
26
+ /** Where the token came from (e.g. "keys/telegram-webhook-secret.txt"). Never contains the token. */
27
+ tokenSource: string | null;
28
+ /** Human readable description of what is missing (never contains a token). */
29
+ missing: string[];
30
+ /** Human readable caveats about the resolved endpoint (never contains a token). */
31
+ notes: string[];
32
+ }
33
+ /** Options for {@link resolveRelayEndpoint}. */
34
+ export interface ResolveRelayEndpointOptions {
35
+ /** Environment to read (defaults to process.env). */
36
+ env?: RelayEndpointEnv;
37
+ /** Directory holding the key files (e.g. ~/.pi-harness-runtime/keys). */
38
+ keysDir: string;
39
+ /** Optional file reader override (used by the tests). */
40
+ readFile?: (path: string) => string | undefined;
41
+ }
42
+ /** Source names, in the order they are consulted. */
43
+ export declare const RELAY_URL_SOURCES: readonly ["env RELAY_WS_URL", "keys/relay-ws-url.txt", "keys/telegram-webhook-url.txt (derived)"];
44
+ export declare const RELAY_TOKEN_SOURCES: readonly ["env RELAY_AUTH_TOKEN", "keys/relay-auth-token.txt", "keys/telegram-webhook-secret.txt"];
45
+ /**
46
+ * The token sources are the SAME chain the relay server uses, so the client
47
+ * and the server can never disagree about the credential.
48
+ *
49
+ * Note: `keys/telegram-webhook-secret.txt` is also the secret the Telegram
50
+ * webhook expects in its `x-telegram-bot-api-secret-token` header. Sharing it
51
+ * with the WebSocket relay widens the blast radius of that secret - put a
52
+ * dedicated token in `keys/relay-auth-token.txt` (it is preferred) when the
53
+ * relay and the webhook do not have to share one.
54
+ *
55
+ * The Telegram webhook secret itself may also come from the environment
56
+ * variable TELEGRAM_WEBHOOK_SECRET; that variable belongs to the HTTP
57
+ * webhook, not to the relay, and is therefore not part of this chain.
58
+ */
59
+ /**
60
+ * Derive the relay WebSocket URL from the Telegram webhook URL.
61
+ *
62
+ * The derivation keeps the host (and port) of the webhook URL and replaces the
63
+ * path with /ws, because the relay is exposed on the same origin as the
64
+ * webhook:
65
+ *
66
+ * https://webhook.bunchee.online/telegram -> wss://webhook.bunchee.online/ws
67
+ *
68
+ * That only works when the origin serves /ws, i.e. behind the reverse proxy
69
+ * that maps /ws to the relay port (nginx: location /ws -> 127.0.0.1:8444).
70
+ * When the webhook URL addresses the application DIRECTLY, the relay listens
71
+ * on WS_PORT (default 8444), not on the HTTP port - use RELAY_WS_URL or
72
+ * keys/relay-ws-url.txt for that setup.
73
+ */
74
+ export declare function deriveRelayWsUrl(webhookUrl: string): string | null;
75
+ /**
76
+ * True when the value looks like a WebSocket URL.
77
+ *
78
+ * Credentials ("wss://user:pass@host") and query strings are rejected: the
79
+ * resolved URL is written to the log, and neither belongs there.
80
+ */
81
+ export declare function isRelayWsUrl(value: string): boolean;
82
+ /**
83
+ * Resolve the relay WebSocket URL and auth token.
84
+ *
85
+ * The first non-empty source wins. A blank file counts as missing (an empty
86
+ * value is a configuration mistake, not a valid endpoint). A source that holds
87
+ * a value which was REJECTED is reported in `missing` so the operator can see
88
+ * why the resolver moved on.
89
+ */
90
+ export declare function resolveRelayEndpoint(options: ResolveRelayEndpointOptions): RelayEndpointResolution;
91
+ /**
92
+ * Log-safe description of a resolution (contains no token value).
93
+ */
94
+ export declare function describeRelayEndpoint(resolution: RelayEndpointResolution): string;
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Relay endpoint resolution.
3
+ *
4
+ * Resolves the relay WebSocket URL and the auth token from the SAME sources
5
+ * the relay server uses (see scripts/unified-webhook-server.ts), so a client
6
+ * and the server always agree:
7
+ *
8
+ * URL: RELAY_WS_URL env -> keys/relay-ws-url.txt -> derived from keys/telegram-webhook-url.txt
9
+ * Token: RELAY_AUTH_TOKEN env -> keys/relay-auth-token.txt -> keys/telegram-webhook-secret.txt
10
+ *
11
+ * The token value is never logged: only the NAME of the source it came from.
12
+ */
13
+ import { readFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ /** Source names, in the order they are consulted. */
16
+ export const RELAY_URL_SOURCES = [
17
+ "env RELAY_WS_URL",
18
+ "keys/relay-ws-url.txt",
19
+ "keys/telegram-webhook-url.txt (derived)",
20
+ ];
21
+ export const RELAY_TOKEN_SOURCES = [
22
+ "env RELAY_AUTH_TOKEN",
23
+ "keys/relay-auth-token.txt",
24
+ "keys/telegram-webhook-secret.txt",
25
+ ];
26
+ /**
27
+ * The token sources are the SAME chain the relay server uses, so the client
28
+ * and the server can never disagree about the credential.
29
+ *
30
+ * Note: `keys/telegram-webhook-secret.txt` is also the secret the Telegram
31
+ * webhook expects in its `x-telegram-bot-api-secret-token` header. Sharing it
32
+ * with the WebSocket relay widens the blast radius of that secret - put a
33
+ * dedicated token in `keys/relay-auth-token.txt` (it is preferred) when the
34
+ * relay and the webhook do not have to share one.
35
+ *
36
+ * The Telegram webhook secret itself may also come from the environment
37
+ * variable TELEGRAM_WEBHOOK_SECRET; that variable belongs to the HTTP
38
+ * webhook, not to the relay, and is therefore not part of this chain.
39
+ */
40
+ /**
41
+ * Derive the relay WebSocket URL from the Telegram webhook URL.
42
+ *
43
+ * The derivation keeps the host (and port) of the webhook URL and replaces the
44
+ * path with /ws, because the relay is exposed on the same origin as the
45
+ * webhook:
46
+ *
47
+ * https://webhook.bunchee.online/telegram -> wss://webhook.bunchee.online/ws
48
+ *
49
+ * That only works when the origin serves /ws, i.e. behind the reverse proxy
50
+ * that maps /ws to the relay port (nginx: location /ws -> 127.0.0.1:8444).
51
+ * When the webhook URL addresses the application DIRECTLY, the relay listens
52
+ * on WS_PORT (default 8444), not on the HTTP port - use RELAY_WS_URL or
53
+ * keys/relay-ws-url.txt for that setup.
54
+ */
55
+ export function deriveRelayWsUrl(webhookUrl) {
56
+ const trimmed = webhookUrl?.trim();
57
+ if (!trimmed)
58
+ return null;
59
+ try {
60
+ const parsed = new URL(trimmed);
61
+ if (!parsed.host)
62
+ return null;
63
+ const secure = parsed.protocol === "https:" || parsed.protocol === "wss:";
64
+ const scheme = secure ? "wss:" : "ws:";
65
+ return `${scheme}//${parsed.host}/ws`;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ /**
72
+ * True when the value looks like a WebSocket URL.
73
+ *
74
+ * Credentials ("wss://user:pass@host") and query strings are rejected: the
75
+ * resolved URL is written to the log, and neither belongs there.
76
+ */
77
+ export function isRelayWsUrl(value) {
78
+ const trimmed = value?.trim() ?? "";
79
+ if (!/^wss?:\/\//i.test(trimmed))
80
+ return false;
81
+ if (trimmed.includes("@") || trimmed.includes("?"))
82
+ return false;
83
+ try {
84
+ const parsed = new URL(trimmed);
85
+ if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:")
86
+ return false;
87
+ return parsed.host.length > 0;
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ function defaultReadFile(path) {
94
+ try {
95
+ return readFileSync(path, "utf-8");
96
+ }
97
+ catch {
98
+ return undefined;
99
+ }
100
+ }
101
+ /** Host of a URL, or an empty string when it cannot be parsed. */
102
+ function hostOf(url) {
103
+ try {
104
+ return new URL(url).host;
105
+ }
106
+ catch {
107
+ return "";
108
+ }
109
+ }
110
+ /** True for hosts that address the machine itself. */
111
+ function isLoopbackHost(host) {
112
+ const name = host.replace(/:\d+$/, "").replace(/^\[|\]$/g, "").toLowerCase();
113
+ return name === "127.0.0.1" || name === "localhost" || name === "::1" || name.startsWith("127.");
114
+ }
115
+ /**
116
+ * Transport warnings for a resolved URL.
117
+ *
118
+ * A plaintext ws:// URL to a remote host sends the auth token in cleartext.
119
+ */
120
+ function transportNotes(url) {
121
+ if (!url.toLowerCase().startsWith("ws://"))
122
+ return [];
123
+ if (isLoopbackHost(hostOf(url)))
124
+ return [];
125
+ return [
126
+ "the relay url is plaintext (ws://) for a remote host - the auth token is sent unencrypted, use wss://",
127
+ ];
128
+ }
129
+ /**
130
+ * Resolve the WebSocket URL: env -> dedicated key file -> derived from the
131
+ * telegram webhook url. The first usable source wins; a source that holds a
132
+ * rejected value is reported so the operator can see why it was skipped.
133
+ */
134
+ function resolveUrl(env, readKey) {
135
+ const rejected = [];
136
+ const notes = [];
137
+ const envUrl = env.RELAY_WS_URL?.trim();
138
+ if (envUrl) {
139
+ if (isRelayWsUrl(envUrl)) {
140
+ notes.push(...transportNotes(envUrl));
141
+ return { url: envUrl, source: "env RELAY_WS_URL", rejected, notes };
142
+ }
143
+ rejected.push("env RELAY_WS_URL (not a ws:// or wss:// URL)");
144
+ }
145
+ const fileUrl = readKey("relay-ws-url.txt");
146
+ if (fileUrl) {
147
+ if (isRelayWsUrl(fileUrl)) {
148
+ notes.push(...transportNotes(fileUrl));
149
+ return { url: fileUrl, source: "keys/relay-ws-url.txt", rejected, notes };
150
+ }
151
+ rejected.push("keys/relay-ws-url.txt (not a ws:// or wss:// URL)");
152
+ }
153
+ const webhookUrl = readKey("telegram-webhook-url.txt");
154
+ const derived = webhookUrl ? deriveRelayWsUrl(webhookUrl) : null;
155
+ if (derived) {
156
+ if (isLoopbackHost(hostOf(derived))) {
157
+ notes.push("derived url points at a loopback host: the relay listens on WS_PORT (default 8444), not on the webhook port - set RELAY_WS_URL or keys/relay-ws-url.txt");
158
+ }
159
+ notes.push(...transportNotes(derived));
160
+ return { url: derived, source: "keys/telegram-webhook-url.txt (derived)", rejected, notes };
161
+ }
162
+ if (webhookUrl) {
163
+ rejected.push("keys/telegram-webhook-url.txt (no ws:// or wss:// URL could be derived)");
164
+ }
165
+ return { url: null, source: null, rejected, notes };
166
+ }
167
+ /** Resolve the auth token: env -> dedicated key file -> webhook secret. */
168
+ function resolveToken(env, readKey) {
169
+ const envToken = env.RELAY_AUTH_TOKEN?.trim();
170
+ if (envToken) {
171
+ return { token: envToken, source: "env RELAY_AUTH_TOKEN" };
172
+ }
173
+ const fileToken = readKey("relay-auth-token.txt");
174
+ if (fileToken) {
175
+ return { token: fileToken, source: "keys/relay-auth-token.txt" };
176
+ }
177
+ const secret = readKey("telegram-webhook-secret.txt");
178
+ if (secret) {
179
+ return { token: secret, source: "keys/telegram-webhook-secret.txt" };
180
+ }
181
+ return { token: null, source: null };
182
+ }
183
+ /**
184
+ * Resolve the relay WebSocket URL and auth token.
185
+ *
186
+ * The first non-empty source wins. A blank file counts as missing (an empty
187
+ * value is a configuration mistake, not a valid endpoint). A source that holds
188
+ * a value which was REJECTED is reported in `missing` so the operator can see
189
+ * why the resolver moved on.
190
+ */
191
+ export function resolveRelayEndpoint(options) {
192
+ const env = options.env ?? process.env;
193
+ const read = options.readFile ?? defaultReadFile;
194
+ const readKey = (name) => {
195
+ try {
196
+ const raw = read(join(options.keysDir, name));
197
+ const value = raw?.trim();
198
+ return value ? value : undefined;
199
+ }
200
+ catch {
201
+ // An unreadable key file is treated as absent - the source list in
202
+ // `missing` tells the operator where to look.
203
+ return undefined;
204
+ }
205
+ };
206
+ const urlResolution = resolveUrl(env, readKey);
207
+ const tokenResolution = resolveToken(env, readKey);
208
+ const missing = [];
209
+ if (!urlResolution.url) {
210
+ missing.push(`url not resolved (looked at ${RELAY_URL_SOURCES.join(", ")})`);
211
+ }
212
+ if (!tokenResolution.token) {
213
+ missing.push(`token not resolved (looked at ${RELAY_TOKEN_SOURCES.join(", ")})`);
214
+ }
215
+ for (const reason of urlResolution.rejected) {
216
+ missing.push(`rejected source: ${reason}`);
217
+ }
218
+ return {
219
+ url: urlResolution.url,
220
+ urlSource: urlResolution.source,
221
+ token: tokenResolution.token,
222
+ tokenSource: tokenResolution.source,
223
+ missing,
224
+ notes: urlResolution.notes,
225
+ };
226
+ }
227
+ /**
228
+ * Log-safe description of a resolution (contains no token value).
229
+ */
230
+ export function describeRelayEndpoint(resolution) {
231
+ const parts = [];
232
+ parts.push(resolution.url ? `url from ${resolution.urlSource}` : "url NOT resolved");
233
+ parts.push(resolution.token ? `token from ${resolution.tokenSource}` : "token NOT resolved");
234
+ let description = parts.join(", ");
235
+ if (resolution.notes.length > 0) {
236
+ description += ` (${resolution.notes.join("; ")})`;
237
+ }
238
+ return description;
239
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/relay",
3
- "version": "1.1.387",
3
+ "version": "1.1.388",
4
4
  "type": "module",
5
5
  "description": "Cloud relay for remote access - T3 Connect style for pi-harness",
6
6
  "main": "./dist/index.js",
@@ -17,6 +17,23 @@ export type { RelayServerOptions } from './server.js';
17
17
  export { RelayClient } from './client.js';
18
18
  export type { RelayClientCallbacks } from './client.js';
19
19
 
20
+ // Endpoint resolution (relay URL + auth token discovery)
21
+ export {
22
+ resolveRelayEndpoint,
23
+ deriveRelayWsUrl,
24
+ isRelayWsUrl,
25
+ describeRelayEndpoint,
26
+ } from './relay-endpoint.js';
27
+ export type {
28
+ RelayEndpointEnv,
29
+ RelayEndpointResolution,
30
+ ResolveRelayEndpointOptions,
31
+ } from './relay-endpoint.js';
32
+ export {
33
+ RELAY_URL_SOURCES,
34
+ RELAY_TOKEN_SOURCES,
35
+ } from './relay-endpoint.js';
36
+
20
37
  // Types
21
38
  export {
22
39
  PROTOCOL_VERSION,