@hahnfeld/msrelay-provider 0.1.2 → 0.1.5
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 +13 -35
- package/dist/plugin.js +142 -71
- package/dist/provider.js +7 -1
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,46 +45,24 @@ This plugin provides a tunnel — it makes a local port reachable via a stable H
|
|
|
45
45
|
openacp plugin install @hahnfeld/msrelay-provider
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
The install wizard
|
|
48
|
+
The install wizard walks you through the full Azure setup — showing the `az` CLI commands for each step and prompting for the values. You'll need the [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) installed.
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
2. Hybrid Connection name (e.g., `bot-endpoint`)
|
|
52
|
-
3. SAS policy name and key (with `Listen` permission)
|
|
53
|
-
4. Local port to forward to (3978 for Bot Framework, 21420 for OpenACP API server, or any custom port)
|
|
50
|
+
The wizard covers:
|
|
54
51
|
|
|
55
|
-
|
|
52
|
+
1. Creating a Relay namespace
|
|
53
|
+
2. Creating a Hybrid Connection (with client authorization disabled)
|
|
54
|
+
3. Creating a listen-only SAS policy
|
|
55
|
+
4. Retrieving the SAS key
|
|
56
|
+
5. Choosing a local port to forward to
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
On reinstall, existing values are pre-filled so you can press Enter to keep them. The SAS key is masked and can be kept without re-entering.
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
# 1. Create Relay namespace
|
|
61
|
-
az relay namespace create \
|
|
62
|
-
--resource-group <rg> --name <namespace> --location westus2
|
|
63
|
-
|
|
64
|
-
# 2. Create Hybrid Connection with client authorization disabled
|
|
65
|
-
# (required if callers can't send SAS headers — Bot Framework, webhooks, etc.)
|
|
66
|
-
# NOTE: --requires-client-authorization cannot be changed after creation.
|
|
67
|
-
# If you need to change it, delete and recreate the Hybrid Connection.
|
|
68
|
-
az relay hyco create \
|
|
69
|
-
--resource-group <rg> --namespace-name <namespace> --name my-connection \
|
|
70
|
-
--requires-client-authorization false
|
|
71
|
-
|
|
72
|
-
# 4. Create listen-only SAS policy (least privilege)
|
|
73
|
-
az relay hyco authorization-rule create \
|
|
74
|
-
--resource-group <rg> --namespace-name <namespace> \
|
|
75
|
-
--hybrid-connection-name my-connection \
|
|
76
|
-
--name ListenOnly --rights Listen
|
|
77
|
-
|
|
78
|
-
# 5. Retrieve the key
|
|
79
|
-
az relay hyco authorization-rule keys list \
|
|
80
|
-
--resource-group <rg> --namespace-name <namespace> \
|
|
81
|
-
--hybrid-connection-name my-connection --name ListenOnly
|
|
82
|
-
```
|
|
60
|
+
After setup, the wizard tests connectivity by opening a real WebSocket to Azure Relay.
|
|
83
61
|
|
|
84
|
-
The resulting endpoint URL is:
|
|
62
|
+
The resulting public endpoint URL is:
|
|
85
63
|
|
|
86
64
|
```
|
|
87
|
-
https://<namespace>.servicebus.windows.net
|
|
65
|
+
https://<namespace>.servicebus.windows.net/<connection>
|
|
88
66
|
```
|
|
89
67
|
|
|
90
68
|
Point your external callers (Bot Framework messaging endpoint, webhook URLs, etc.) at this URL.
|
|
@@ -111,13 +89,13 @@ openacp plugin configure @hahnfeld/msrelay-provider
|
|
|
111
89
|
| Command | Description |
|
|
112
90
|
|---------|-------------|
|
|
113
91
|
| `/relay` | Show connection status, uptime, request/error counts |
|
|
114
|
-
| `/relay auth` |
|
|
92
|
+
| `/relay auth` | Test connectivity by connecting to Azure Relay |
|
|
115
93
|
|
|
116
94
|
## Development
|
|
117
95
|
|
|
118
96
|
```bash
|
|
119
97
|
pnpm install
|
|
120
|
-
pnpm test # Run tests (
|
|
98
|
+
pnpm test # Run tests (42 tests)
|
|
121
99
|
pnpm build # Compile TypeScript
|
|
122
100
|
pnpm dev # Watch mode
|
|
123
101
|
pnpm lint # Type-check without emitting
|
package/dist/plugin.js
CHANGED
|
@@ -10,20 +10,41 @@ function isValidNamespace(v) {
|
|
|
10
10
|
function isValidConnectionName(v) {
|
|
11
11
|
return /^[a-zA-Z0-9._-]+$/.test(v);
|
|
12
12
|
}
|
|
13
|
-
/** Test
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
/** Test connectivity by opening a real WebSocket to Azure Relay. */
|
|
14
|
+
function testRelayConnection(namespace, connectionName, keyName, keyValue, timeoutMs = 10_000) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
try {
|
|
17
|
+
const hycoHttps = require("hyco-https");
|
|
18
|
+
const uri = hycoHttps.createRelayListenUri(namespace, connectionName);
|
|
19
|
+
const server = hycoHttps.createRelayedServer({ server: uri, token: () => hycoHttps.createRelayToken(uri, keyName, keyValue) }, () => { });
|
|
20
|
+
let settled = false;
|
|
21
|
+
const settle = (result) => {
|
|
22
|
+
if (!settled) {
|
|
23
|
+
settled = true;
|
|
24
|
+
try {
|
|
25
|
+
server.close();
|
|
26
|
+
}
|
|
27
|
+
catch { /* ignore */ }
|
|
28
|
+
resolve(result);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const timer = setTimeout(() => {
|
|
32
|
+
settle({ ok: false, error: "Connection timed out — check namespace, connection name, and network access" });
|
|
33
|
+
}, timeoutMs);
|
|
34
|
+
server.on("listening", () => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
settle({ ok: true });
|
|
37
|
+
});
|
|
38
|
+
server.on("error", (err) => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
settle({ ok: false, error: `Connection failed: ${err.message}` });
|
|
41
|
+
});
|
|
42
|
+
server.listen();
|
|
21
43
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
resolve({ ok: false, error: `Setup failed: ${err.message}` });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
27
48
|
}
|
|
28
49
|
/** Format elapsed time as human-readable duration. */
|
|
29
50
|
function formatUptime(startedAt) {
|
|
@@ -64,7 +85,7 @@ export function createRelayPlugin() {
|
|
|
64
85
|
let provider = null;
|
|
65
86
|
return {
|
|
66
87
|
name: "@hahnfeld/msrelay-provider",
|
|
67
|
-
version: "0.1.
|
|
88
|
+
version: "0.1.5",
|
|
68
89
|
description: "Azure Relay Hybrid Connections tunnel provider — private HTTP tunneling via Azure backbone",
|
|
69
90
|
essential: false,
|
|
70
91
|
permissions: [
|
|
@@ -77,30 +98,42 @@ export function createRelayPlugin() {
|
|
|
77
98
|
// ─── Interactive Install Wizard ──────────────────────────────────────
|
|
78
99
|
async install(ctx) {
|
|
79
100
|
const { terminal, settings } = ctx;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
" 2. Hybrid Connection with client authorization DISABLED\n" +
|
|
86
|
-
" 3. SAS key with Listen permission\n" +
|
|
101
|
+
// Load existing config for pre-filling on reinstall
|
|
102
|
+
const existing = await settings.getAll();
|
|
103
|
+
terminal.note("Azure Relay Hybrid Connections exposes a local port via a stable\n" +
|
|
104
|
+
"HTTPS endpoint on Azure's private backbone — no public internet\n" +
|
|
105
|
+
"exposure, no inbound ports required.\n" +
|
|
87
106
|
"\n" +
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
107
|
+
"This wizard collects a few names, then gives you the exact Azure\n" +
|
|
108
|
+
"CLI commands to run. You'll need the az CLI installed.", "Azure Relay Setup");
|
|
109
|
+
// ── Step 1: Collect names ──
|
|
110
|
+
const resourceGroup = await terminal.text({
|
|
111
|
+
message: "Azure resource group:",
|
|
112
|
+
defaultValue: existing.resourceGroup ?? undefined,
|
|
92
113
|
validate: (v) => {
|
|
93
|
-
|
|
94
|
-
|
|
114
|
+
if (!v.trim())
|
|
115
|
+
return "Resource group is required";
|
|
116
|
+
return undefined;
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
const rg = resourceGroup.trim();
|
|
120
|
+
const namespaceInput = await terminal.text({
|
|
121
|
+
message: "Relay namespace name (becomes <name>.servicebus.windows.net):",
|
|
122
|
+
defaultValue: existing.relayNamespace?.replace(".servicebus.windows.net", "") ?? undefined,
|
|
123
|
+
validate: (v) => {
|
|
124
|
+
if (!v.trim())
|
|
95
125
|
return "Namespace is required";
|
|
96
|
-
if (!isValidNamespace(trimmed))
|
|
97
|
-
return "Must be <name>.servicebus.windows.net";
|
|
98
126
|
return undefined;
|
|
99
127
|
},
|
|
100
128
|
});
|
|
101
|
-
|
|
129
|
+
const nsName = namespaceInput.trim().replace(".servicebus.windows.net", "");
|
|
130
|
+
const ns = `${nsName}.servicebus.windows.net`;
|
|
131
|
+
if (!isValidNamespace(ns)) {
|
|
132
|
+
terminal.log.warning(`"${ns}" doesn't look like a valid namespace — continuing anyway`);
|
|
133
|
+
}
|
|
102
134
|
const connectionName = await terminal.text({
|
|
103
|
-
message: "Hybrid Connection name
|
|
135
|
+
message: "Hybrid Connection name:",
|
|
136
|
+
defaultValue: existing.hybridConnectionName ?? "bot-endpoint",
|
|
104
137
|
validate: (v) => {
|
|
105
138
|
const trimmed = v.trim();
|
|
106
139
|
if (!trimmed)
|
|
@@ -110,38 +143,72 @@ export function createRelayPlugin() {
|
|
|
110
143
|
return undefined;
|
|
111
144
|
},
|
|
112
145
|
});
|
|
113
|
-
|
|
146
|
+
const hc = connectionName.trim();
|
|
114
147
|
const keyName = await terminal.text({
|
|
115
|
-
message: "SAS policy name:",
|
|
116
|
-
defaultValue: "ListenOnly",
|
|
148
|
+
message: "SAS policy name (listen-only auth rule):",
|
|
149
|
+
defaultValue: existing.sasKeyName ?? "ListenOnly",
|
|
117
150
|
validate: (v) => {
|
|
118
151
|
if (!v.trim())
|
|
119
|
-
return "
|
|
152
|
+
return "Policy name is required";
|
|
120
153
|
return undefined;
|
|
121
154
|
},
|
|
122
155
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
156
|
+
const sasName = keyName.trim();
|
|
157
|
+
// ── Step 2: Show the public URL and exact az commands ──
|
|
158
|
+
terminal.log.step("Your public endpoint will be:");
|
|
159
|
+
terminal.log.info(` https://${ns}/${hc}/api/messages`);
|
|
160
|
+
terminal.log.info("");
|
|
161
|
+
terminal.log.step("Run these commands to create the Azure resources:");
|
|
162
|
+
terminal.log.info("");
|
|
163
|
+
terminal.log.info(`az relay namespace create \\`);
|
|
164
|
+
terminal.log.info(` --resource-group ${rg} --name ${nsName} --location westus2`);
|
|
165
|
+
terminal.log.info("");
|
|
166
|
+
terminal.log.info(`az relay hyco create \\`);
|
|
167
|
+
terminal.log.info(` --resource-group ${rg} --namespace-name ${nsName} \\`);
|
|
168
|
+
terminal.log.info(` --name ${hc} --requires-client-authorization false`);
|
|
169
|
+
terminal.log.info("");
|
|
170
|
+
terminal.log.info(`az relay hyco authorization-rule create \\`);
|
|
171
|
+
terminal.log.info(` --resource-group ${rg} --namespace-name ${nsName} \\`);
|
|
172
|
+
terminal.log.info(` --hybrid-connection-name ${hc} \\`);
|
|
173
|
+
terminal.log.info(` --name ${sasName} --rights Listen`);
|
|
174
|
+
terminal.log.info("");
|
|
175
|
+
terminal.log.warning("--requires-client-authorization cannot be changed after creation.");
|
|
176
|
+
terminal.log.warning("If you need to change it, delete and recreate the connection.");
|
|
177
|
+
terminal.log.info("");
|
|
178
|
+
// ── Step 3: SAS Key ──
|
|
179
|
+
terminal.log.step("Retrieve the SAS key:");
|
|
180
|
+
terminal.log.info("");
|
|
181
|
+
terminal.log.info(`az relay hyco authorization-rule keys list \\`);
|
|
182
|
+
terminal.log.info(` --resource-group ${rg} --namespace-name ${nsName} \\`);
|
|
183
|
+
terminal.log.info(` --hybrid-connection-name ${hc} --name ${sasName} \\`);
|
|
184
|
+
terminal.log.info(` --query primaryKey -o tsv`);
|
|
185
|
+
terminal.log.info("");
|
|
186
|
+
const hasExistingKey = !!(existing.sasKeyValue);
|
|
187
|
+
let keyValue = await terminal.password({
|
|
188
|
+
message: hasExistingKey
|
|
189
|
+
? "SAS key value — press Enter to keep current:"
|
|
190
|
+
: "SAS key value (paste the primaryKey):",
|
|
126
191
|
validate: (v) => {
|
|
127
|
-
if (!v.trim())
|
|
192
|
+
if (!(v ?? "").trim() && !hasExistingKey)
|
|
128
193
|
return "Key value is required";
|
|
129
194
|
return undefined;
|
|
130
195
|
},
|
|
131
196
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
" 21420 — OpenACP API server\n" +
|
|
138
|
-
"\n" +
|
|
139
|
-
"You can change this later with: openacp plugin configure @hahnfeld/msrelay-provider", "Port Selection");
|
|
197
|
+
if (!(keyValue ?? "").trim() && hasExistingKey) {
|
|
198
|
+
keyValue = existing.sasKeyValue;
|
|
199
|
+
}
|
|
200
|
+
// ── Step 4: Port ──
|
|
201
|
+
const existingPort = existing.port;
|
|
140
202
|
const envPort = process.env.PORT ? Number(process.env.PORT) : null;
|
|
141
203
|
const portChoice = await terminal.select({
|
|
142
|
-
message: "Which port should the tunnel
|
|
204
|
+
message: "Which local port should the tunnel forward to?",
|
|
143
205
|
options: [
|
|
144
|
-
...(
|
|
206
|
+
...(existingPort && existingPort !== 3978 && existingPort !== 21420
|
|
207
|
+
? [{ value: String(existingPort), label: `${existingPort} (current config)`, hint: "Previously configured" }]
|
|
208
|
+
: []),
|
|
209
|
+
...(envPort && envPort !== existingPort
|
|
210
|
+
? [{ value: String(envPort), label: `${envPort} (from PORT env var)`, hint: "Current environment" }]
|
|
211
|
+
: []),
|
|
145
212
|
{ value: "3978", label: "3978", hint: "Teams adapter (Bot Framework default)" },
|
|
146
213
|
{ value: "21420", label: "21420", hint: "OpenACP API server" },
|
|
147
214
|
{ value: "custom", label: "Other port" },
|
|
@@ -151,6 +218,7 @@ export function createRelayPlugin() {
|
|
|
151
218
|
if (portChoice === "custom") {
|
|
152
219
|
const portStr = await terminal.text({
|
|
153
220
|
message: "Local port to expose:",
|
|
221
|
+
defaultValue: existingPort ? String(existingPort) : undefined,
|
|
154
222
|
validate: (v) => {
|
|
155
223
|
const trimmed = v.trim();
|
|
156
224
|
if (!trimmed)
|
|
@@ -166,39 +234,42 @@ export function createRelayPlugin() {
|
|
|
166
234
|
else {
|
|
167
235
|
port = Number(portChoice);
|
|
168
236
|
}
|
|
169
|
-
// ── Step
|
|
237
|
+
// ── Step 5: Connectivity Test ──
|
|
170
238
|
const wantTest = await terminal.confirm({
|
|
171
|
-
message: "Test connectivity now? (
|
|
172
|
-
initialValue:
|
|
239
|
+
message: "Test connectivity now? (connects to Azure Relay for up to 10s)",
|
|
240
|
+
initialValue: true,
|
|
173
241
|
});
|
|
174
242
|
if (wantTest) {
|
|
175
243
|
const spin = terminal.spinner();
|
|
176
|
-
spin.start("
|
|
177
|
-
const
|
|
178
|
-
if (
|
|
179
|
-
spin.stop("
|
|
244
|
+
spin.start("Connecting to Azure Relay...");
|
|
245
|
+
const result = await testRelayConnection(ns, hc, sasName, keyValue.trim());
|
|
246
|
+
if (result.ok) {
|
|
247
|
+
spin.stop("Connected to Azure Relay successfully");
|
|
180
248
|
}
|
|
181
249
|
else {
|
|
182
|
-
spin.fail(
|
|
250
|
+
spin.fail(result.error);
|
|
183
251
|
terminal.log.warning("Installation will continue — you can test connectivity later with /relay auth");
|
|
184
252
|
}
|
|
185
253
|
}
|
|
186
|
-
// ── Step
|
|
254
|
+
// ── Step 6: Save ──
|
|
187
255
|
await settings.setAll({
|
|
188
256
|
enabled: true,
|
|
189
257
|
port,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
258
|
+
resourceGroup: rg,
|
|
259
|
+
relayNamespace: ns,
|
|
260
|
+
hybridConnectionName: hc,
|
|
261
|
+
sasKeyName: sasName,
|
|
193
262
|
sasKeyValue: keyValue.trim(),
|
|
194
263
|
});
|
|
195
264
|
terminal.log.success("Azure Relay provider configured!");
|
|
196
265
|
terminal.log.info("");
|
|
197
|
-
terminal.note(`Namespace: ${
|
|
198
|
-
`Connection: ${
|
|
199
|
-
`SAS policy: ${
|
|
266
|
+
terminal.note(`Namespace: ${ns}\n` +
|
|
267
|
+
`Connection: ${hc}\n` +
|
|
268
|
+
`SAS policy: ${sasName}\n` +
|
|
200
269
|
`Port: ${port}\n` +
|
|
201
|
-
|
|
270
|
+
"\n" +
|
|
271
|
+
"Set your Bot Framework messaging endpoint to:\n" +
|
|
272
|
+
` https://${ns}/${hc}/api/messages`, "Configuration Summary");
|
|
202
273
|
},
|
|
203
274
|
// ─── Configure (post-install changes) ────────────────────────────────
|
|
204
275
|
async configure(ctx) {
|
|
@@ -270,7 +341,7 @@ export function createRelayPlugin() {
|
|
|
270
341
|
break;
|
|
271
342
|
}
|
|
272
343
|
case "sasKeyValue": {
|
|
273
|
-
const val = await terminal.
|
|
344
|
+
const val = await terminal.password({
|
|
274
345
|
message: "SAS key value:",
|
|
275
346
|
validate: (v) => {
|
|
276
347
|
if (!v.trim())
|
|
@@ -308,10 +379,10 @@ export function createRelayPlugin() {
|
|
|
308
379
|
}
|
|
309
380
|
case "auth": {
|
|
310
381
|
const spin = terminal.spinner();
|
|
311
|
-
spin.start("
|
|
312
|
-
const result =
|
|
382
|
+
spin.start("Connecting to Azure Relay...");
|
|
383
|
+
const result = await testRelayConnection(current.relayNamespace, current.hybridConnectionName, current.sasKeyName, current.sasKeyValue);
|
|
313
384
|
if (result.ok) {
|
|
314
|
-
spin.stop("
|
|
385
|
+
spin.stop("Connected to Azure Relay successfully");
|
|
315
386
|
}
|
|
316
387
|
else {
|
|
317
388
|
spin.fail(result.error);
|
|
@@ -394,9 +465,9 @@ export function createRelayPlugin() {
|
|
|
394
465
|
const raw = args.raw.trim();
|
|
395
466
|
if (raw === "auth") {
|
|
396
467
|
const cfg = config;
|
|
397
|
-
const result =
|
|
468
|
+
const result = await testRelayConnection(cfg.relayNamespace, cfg.hybridConnectionName, cfg.sasKeyName, cfg.sasKeyValue);
|
|
398
469
|
if (result.ok) {
|
|
399
|
-
return { type: "text", text: "
|
|
470
|
+
return { type: "text", text: "Azure Relay connectivity: OK" };
|
|
400
471
|
}
|
|
401
472
|
return { type: "error", message: result.error };
|
|
402
473
|
}
|
package/dist/provider.js
CHANGED
|
@@ -234,12 +234,18 @@ export class AzureRelayProvider {
|
|
|
234
234
|
this.sendError(relayRes, 400, "Bad Request", `Invalid request URL: ${rawUrl}`);
|
|
235
235
|
return;
|
|
236
236
|
}
|
|
237
|
+
// Azure Relay prepends the Hybrid Connection name to the path.
|
|
238
|
+
// e.g., /bot-endpoint/api/messages → /api/messages
|
|
239
|
+
const prefix = `/${this.config.hybridConnectionName}`;
|
|
240
|
+
const forwardPath = rawUrl.startsWith(prefix)
|
|
241
|
+
? (rawUrl.slice(prefix.length) || "/")
|
|
242
|
+
: rawUrl;
|
|
237
243
|
const forwardHeaders = stripHopByHop(relayReq.headers);
|
|
238
244
|
forwardHeaders["host"] = `localhost:${this.localPort}`;
|
|
239
245
|
const localReq = httpRequest({
|
|
240
246
|
hostname: "localhost",
|
|
241
247
|
port: this.localPort,
|
|
242
|
-
path:
|
|
248
|
+
path: forwardPath,
|
|
243
249
|
method: relayReq.method,
|
|
244
250
|
headers: forwardHeaders,
|
|
245
251
|
timeout: LOCAL_FORWARD_TIMEOUT_MS,
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface AzureRelayConfig {
|
|
|
4
4
|
enabled: boolean;
|
|
5
5
|
/** Local port to forward requests to. Default: PORT env var or 3978. */
|
|
6
6
|
port: number | null;
|
|
7
|
+
/** Azure resource group (saved for wizard pre-fill, not used at runtime). */
|
|
8
|
+
resourceGroup?: string;
|
|
7
9
|
/** Azure Relay namespace (e.g., "myrelay.servicebus.windows.net"). */
|
|
8
10
|
relayNamespace: string;
|
|
9
11
|
/** Hybrid Connection name (e.g., "bot-endpoint"). */
|