@kortix/agent-tunnel 0.1.1 → 0.1.3
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/dist/agent/index.d.ts +140 -0
- package/dist/agent/index.js +21 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent-cli.js +4061 -0
- package/dist/chunk-7N7GSU6K.js +34 -0
- package/dist/client/index.d.ts +183 -0
- package/dist/client/index.js +8 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client-cli.js +752 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.d.ts +89 -0
- package/dist/server/index.js +14 -0
- package/dist/server/index.js.map +1 -0
- package/dist/shared/index.d.ts +10 -0
- package/dist/shared/index.js +20 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/types-Dpwrd8Ai.d.ts +194 -0
- package/package.json +32 -12
- package/src/agent/agent.ts +19 -9
- package/src/agent/capabilities/desktop/atspi-helper.ts +1 -1
- package/src/agent/capabilities/desktop/csharp-helper.ts +1 -1
- package/src/agent/capabilities/desktop/linux-driver.ts +1 -1
- package/src/agent/capabilities/desktop/macos-driver.ts +1 -1
- package/src/agent/capabilities/desktop/swift-helper.ts +1 -1
- package/src/agent/cli.ts +157 -23
- package/src/client/cli.test.ts +10 -10
- package/src/client/cli.ts +5 -6
- package/src/node-ws-polyfill.ts +17 -0
- package/src/server/ws-handler.ts +5 -0
- package/src/shared/crypto.ts +2 -3
package/src/agent/cli.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
import { loadConfig } from './config';
|
|
1
|
+
import '../node-ws-polyfill';
|
|
2
|
+
import { loadConfig, type TunnelConfig } from './config';
|
|
3
3
|
import { TunnelAgent } from './agent';
|
|
4
4
|
import { CapabilityRegistry } from './capabilities/index';
|
|
5
5
|
import { createFilesystemCapability } from './capabilities/filesystem';
|
|
6
6
|
import { createShellCapability } from './capabilities/shell';
|
|
7
7
|
import { createDesktopCapability } from './capabilities/desktop';
|
|
8
8
|
import { hostname, platform, arch, release } from 'os';
|
|
9
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import { execSync } from 'child_process';
|
|
9
13
|
|
|
10
14
|
const c = {
|
|
11
15
|
reset: '\x1b[0m',
|
|
@@ -114,30 +118,14 @@ async function printStartup(config: { tunnelId: string; apiUrl: string }, capabi
|
|
|
114
118
|
console.log('');
|
|
115
119
|
}
|
|
116
120
|
|
|
117
|
-
|
|
118
|
-
const config = loadConfig({
|
|
119
|
-
token: flags.token,
|
|
120
|
-
tunnelId: flags['tunnel-id'],
|
|
121
|
-
apiUrl: flags['api-url'],
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
if (!config.token) {
|
|
125
|
-
console.error(`${c.red}${c.bold} error${c.reset} --token is required`);
|
|
126
|
-
process.exit(1);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (!config.tunnelId) {
|
|
130
|
-
console.error(`${c.red}${c.bold} error${c.reset} --tunnel-id is required`);
|
|
131
|
-
process.exit(1);
|
|
132
|
-
}
|
|
133
|
-
|
|
121
|
+
function startAgent(config: TunnelConfig): void {
|
|
134
122
|
const registry = new CapabilityRegistry();
|
|
135
123
|
registry.register(createFilesystemCapability(config));
|
|
136
124
|
registry.register(createShellCapability(config));
|
|
137
125
|
registry.register(createDesktopCapability());
|
|
138
126
|
|
|
139
127
|
clearScreen();
|
|
140
|
-
|
|
128
|
+
printStartup(config, registry.getCapabilityNames(), '0.1.2');
|
|
141
129
|
|
|
142
130
|
const agent = new TunnelAgent(config, registry);
|
|
143
131
|
agent.connect();
|
|
@@ -152,6 +140,152 @@ async function commandConnect(flags: Record<string, string>): Promise<void> {
|
|
|
152
140
|
process.on('SIGINT', shutdown);
|
|
153
141
|
}
|
|
154
142
|
|
|
143
|
+
function openBrowser(url: string): void {
|
|
144
|
+
try {
|
|
145
|
+
const plat = platform();
|
|
146
|
+
if (plat === 'darwin') execSync(`open "${url}"`);
|
|
147
|
+
else if (plat === 'win32') execSync(`start "" "${url}"`);
|
|
148
|
+
else execSync(`xdg-open "${url}"`);
|
|
149
|
+
} catch {}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const CONFIG_DIR = join(homedir(), '.agent-tunnel');
|
|
153
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
154
|
+
|
|
155
|
+
function saveCredentials(tunnelId: string, token: string, apiUrl: string): void {
|
|
156
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
157
|
+
let existing: Record<string, unknown> = {};
|
|
158
|
+
if (existsSync(CONFIG_FILE)) {
|
|
159
|
+
try { existing = JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')); } catch {}
|
|
160
|
+
}
|
|
161
|
+
writeFileSync(CONFIG_FILE, JSON.stringify({ ...existing, tunnelId, token, apiUrl }, null, 2));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function commandConnectDeviceAuth(config: TunnelConfig): Promise<void> {
|
|
165
|
+
console.log('');
|
|
166
|
+
console.log(` ${c.cyan}◆${c.reset} ${c.bold}Device Authorization${c.reset}`);
|
|
167
|
+
console.log('');
|
|
168
|
+
|
|
169
|
+
// Step 1: Create device auth request
|
|
170
|
+
let deviceCode: string;
|
|
171
|
+
let deviceSecret: string;
|
|
172
|
+
let verificationUrl: string;
|
|
173
|
+
let expiresAt: string;
|
|
174
|
+
let pollIntervalMs: number;
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const res = await fetch(`${config.apiUrl}/device-auth`, {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers: { 'Content-Type': 'application/json' },
|
|
180
|
+
body: JSON.stringify({ machineHostname: hostname() }),
|
|
181
|
+
});
|
|
182
|
+
if (!res.ok) {
|
|
183
|
+
const text = await res.text().catch(() => '');
|
|
184
|
+
console.error(` ${c.red}✗${c.reset} Failed to create device auth request: ${res.status} ${text.slice(0, 200)}`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const data = await res.json();
|
|
188
|
+
deviceCode = data.deviceCode;
|
|
189
|
+
deviceSecret = data.deviceSecret;
|
|
190
|
+
verificationUrl = data.verificationUrl;
|
|
191
|
+
expiresAt = data.expiresAt;
|
|
192
|
+
pollIntervalMs = data.pollIntervalMs || 2000;
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error(` ${c.red}✗${c.reset} Failed to reach API at ${config.apiUrl}`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Step 2: Display code and open browser
|
|
200
|
+
console.log(` ${c.dim}Code:${c.reset} ${c.bold}${c.white}${deviceCode}${c.reset}`);
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log(` ${c.dim}Open this URL on any device to approve:${c.reset}`);
|
|
203
|
+
console.log(` ${c.cyan}${verificationUrl}${c.reset}`);
|
|
204
|
+
console.log('');
|
|
205
|
+
|
|
206
|
+
openBrowser(verificationUrl);
|
|
207
|
+
|
|
208
|
+
// Step 3: Poll for approval
|
|
209
|
+
const expiresAtMs = new Date(expiresAt).getTime();
|
|
210
|
+
|
|
211
|
+
while (true) {
|
|
212
|
+
const remaining = Math.max(0, Math.floor((expiresAtMs - Date.now()) / 1000));
|
|
213
|
+
if (remaining <= 0) {
|
|
214
|
+
console.log(`\n ${c.red}✗${c.reset} Authorization expired. Please try again.`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const min = Math.floor(remaining / 60);
|
|
219
|
+
const sec = remaining % 60;
|
|
220
|
+
process.stdout.write(`\r ${c.dim}Waiting for approval... ${c.white}${min}:${sec.toString().padStart(2, '0')}${c.reset} `);
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const res = await fetch(`${config.apiUrl}/device-auth/${deviceCode}/status?secret=${deviceSecret}`);
|
|
224
|
+
if (res.ok) {
|
|
225
|
+
const data = await res.json();
|
|
226
|
+
|
|
227
|
+
if (data.status === 'approved' && data.tunnelId && data.token) {
|
|
228
|
+
process.stdout.write('\r' + ' '.repeat(60) + '\r');
|
|
229
|
+
console.log(` ${c.green}●${c.reset} ${c.bold}Authorized!${c.reset}`);
|
|
230
|
+
console.log('');
|
|
231
|
+
|
|
232
|
+
// Save credentials
|
|
233
|
+
saveCredentials(data.tunnelId, data.token, config.apiUrl);
|
|
234
|
+
console.log(` ${c.dim}Credentials saved to ${CONFIG_FILE}${c.reset}`);
|
|
235
|
+
console.log('');
|
|
236
|
+
|
|
237
|
+
// Connect with received credentials
|
|
238
|
+
const fullConfig = loadConfig({
|
|
239
|
+
token: data.token,
|
|
240
|
+
tunnelId: data.tunnelId,
|
|
241
|
+
apiUrl: config.apiUrl,
|
|
242
|
+
});
|
|
243
|
+
startAgent(fullConfig);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (data.status === 'denied') {
|
|
248
|
+
process.stdout.write('\r' + ' '.repeat(60) + '\r');
|
|
249
|
+
console.log(` ${c.red}✗${c.reset} Authorization denied.`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (data.status === 'expired') {
|
|
254
|
+
process.stdout.write('\r' + ' '.repeat(60) + '\r');
|
|
255
|
+
console.log(` ${c.red}✗${c.reset} Authorization expired. Please try again.`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch {}
|
|
260
|
+
|
|
261
|
+
await sleep(pollIntervalMs);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function commandConnect(flags: Record<string, string>): Promise<void> {
|
|
266
|
+
const config = loadConfig({
|
|
267
|
+
token: flags.token,
|
|
268
|
+
tunnelId: flags['tunnel-id'],
|
|
269
|
+
apiUrl: flags['api-url'],
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// If both token and tunnelId are provided, connect directly
|
|
273
|
+
if (config.token && config.tunnelId) {
|
|
274
|
+
startAgent(config);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// If neither is provided, use device auth flow
|
|
279
|
+
if (!config.token && !config.tunnelId) {
|
|
280
|
+
await commandConnectDeviceAuth(config);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Partial — error
|
|
285
|
+
console.error(`${c.red}${c.bold} error${c.reset} Provide both --token and --tunnel-id, or neither (for device auth)`);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
|
|
155
289
|
async function commandStatus(flags: Record<string, string>): Promise<void> {
|
|
156
290
|
const config = loadConfig({
|
|
157
291
|
token: flags.token,
|
|
@@ -192,13 +326,13 @@ function showHelp(): void {
|
|
|
192
326
|
console.log(` ${c.bold}Usage${c.reset} ${c.dim}npx @kortix/agent-tunnel <command> [options]${c.reset}`);
|
|
193
327
|
console.log('');
|
|
194
328
|
console.log(`${c.gray} ── Commands ────────────────────────────────────────${c.reset}`);
|
|
195
|
-
console.log(` ${c.cyan}connect${c.reset} Connect
|
|
329
|
+
console.log(` ${c.cyan}connect${c.reset} Connect via device auth (opens browser)`);
|
|
196
330
|
console.log(` ${c.cyan}status${c.reset} Check tunnel connection status`);
|
|
197
331
|
console.log(` ${c.cyan}help${c.reset} Show this help message`);
|
|
198
332
|
console.log('');
|
|
199
333
|
console.log(`${c.gray} ── Options ─────────────────────────────────────────${c.reset}`);
|
|
200
|
-
console.log(` ${c.white}--token${c.reset} ${c.dim}<token>${c.reset}
|
|
201
|
-
console.log(` ${c.white}--tunnel-id${c.reset} ${c.dim}<id>${c.reset} Tunnel ID ${c.dim}(
|
|
334
|
+
console.log(` ${c.white}--token${c.reset} ${c.dim}<token>${c.reset} Skip device auth, connect directly`);
|
|
335
|
+
console.log(` ${c.white}--tunnel-id${c.reset} ${c.dim}<id>${c.reset} Tunnel ID ${c.dim}(required with --token)${c.reset}`);
|
|
202
336
|
console.log(` ${c.white}--api-url${c.reset} ${c.dim}<url>${c.reset} API URL ${c.dim}(default: http://localhost:8080)${c.reset}`);
|
|
203
337
|
console.log('');
|
|
204
338
|
console.log(` ${c.dim}Config: ~/.agent-tunnel/config.json${c.reset}`);
|
package/src/client/cli.test.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Strategy: spin up a lightweight mock HTTP server that simulates the
|
|
5
5
|
* tunnel relay API, then invoke the CLI as a child process via `bun run`
|
|
6
|
-
* with
|
|
6
|
+
* with TUNNEL_API_URL pointing at the mock. Assert on JSON stdout,
|
|
7
7
|
* stderr, and exit codes.
|
|
8
8
|
*
|
|
9
9
|
* Run: bun test src/client/cli.test.ts
|
|
@@ -267,9 +267,9 @@ function runCli(
|
|
|
267
267
|
const child = spawn("bun", cliArgs, {
|
|
268
268
|
env: {
|
|
269
269
|
...process.env,
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
270
|
+
TUNNEL_API_URL: `http://localhost:${mockPort}`,
|
|
271
|
+
TUNNEL_TOKEN: "test-token",
|
|
272
|
+
TUNNEL_ID: "",
|
|
273
273
|
...envOverrides,
|
|
274
274
|
},
|
|
275
275
|
cwd: dirname(dirname(CLI_PATH)),
|
|
@@ -669,7 +669,7 @@ describe("Agent Tunnel CLI", () => {
|
|
|
669
669
|
|
|
670
670
|
test("permission denied returns structured response", async () => {
|
|
671
671
|
const r = await runCli("fs_read", '{"path":"/etc/passwd"}', {
|
|
672
|
-
|
|
672
|
+
TUNNEL_API_URL: `http://localhost:${permPort}`,
|
|
673
673
|
});
|
|
674
674
|
// CLI should output permission-required JSON (not crash)
|
|
675
675
|
expect(r.json).not.toBeNull();
|
|
@@ -683,7 +683,7 @@ describe("Agent Tunnel CLI", () => {
|
|
|
683
683
|
const r = await runCli(
|
|
684
684
|
"shell",
|
|
685
685
|
'{"command":"rm","args":["-rf","/"]}',
|
|
686
|
-
{
|
|
686
|
+
{ TUNNEL_API_URL: `http://localhost:${permPort}` }
|
|
687
687
|
);
|
|
688
688
|
expect(r.json!.success).toBe(false);
|
|
689
689
|
expect(r.json!.permissionRequired).toBe(true);
|
|
@@ -695,7 +695,7 @@ describe("Agent Tunnel CLI", () => {
|
|
|
695
695
|
describe("server unreachable", () => {
|
|
696
696
|
test("status with dead server returns error JSON", async () => {
|
|
697
697
|
const r = await runCli("status", undefined, {
|
|
698
|
-
|
|
698
|
+
TUNNEL_API_URL: "http://localhost:1",
|
|
699
699
|
});
|
|
700
700
|
expect(r.exitCode).toBe(1);
|
|
701
701
|
expect(r.json).not.toBeNull();
|
|
@@ -705,7 +705,7 @@ describe("Agent Tunnel CLI", () => {
|
|
|
705
705
|
|
|
706
706
|
test("fs_read with dead server returns error JSON", async () => {
|
|
707
707
|
const r = await runCli("fs_read", '{"path":"/tmp/x"}', {
|
|
708
|
-
|
|
708
|
+
TUNNEL_API_URL: "http://localhost:1",
|
|
709
709
|
});
|
|
710
710
|
expect(r.exitCode).toBe(1);
|
|
711
711
|
expect(r.json!.success).toBe(false);
|
|
@@ -738,7 +738,7 @@ describe("Agent Tunnel CLI", () => {
|
|
|
738
738
|
|
|
739
739
|
test("status with no connections returns empty list", async () => {
|
|
740
740
|
const r = await runCli("status", undefined, {
|
|
741
|
-
|
|
741
|
+
TUNNEL_API_URL: `http://localhost:${emptyPort}`,
|
|
742
742
|
});
|
|
743
743
|
expect(r.exitCode).toBe(0);
|
|
744
744
|
expect(r.json!.success).toBe(true);
|
|
@@ -748,7 +748,7 @@ describe("Agent Tunnel CLI", () => {
|
|
|
748
748
|
|
|
749
749
|
test("fs_read with no connections returns error", async () => {
|
|
750
750
|
const r = await runCli("fs_read", '{"path":"/tmp/x"}', {
|
|
751
|
-
|
|
751
|
+
TUNNEL_API_URL: `http://localhost:${emptyPort}`,
|
|
752
752
|
});
|
|
753
753
|
expect(r.exitCode).toBe(1);
|
|
754
754
|
expect(r.json!.success).toBe(false);
|
package/src/client/cli.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
1
|
/**
|
|
3
2
|
* Agent Tunnel CLI — interact with the user's local machine via Agent Tunnel.
|
|
4
3
|
*
|
|
5
|
-
* Usage:
|
|
4
|
+
* Usage: agent-tunnel-cli <command> [args as JSON]
|
|
6
5
|
*
|
|
7
6
|
* Commands:
|
|
8
7
|
* status — list all tunnel connections
|
|
@@ -56,15 +55,15 @@ function getEnv(key: string): string | undefined {
|
|
|
56
55
|
const FALLBACK_API_URL = "http://localhost:8008";
|
|
57
56
|
|
|
58
57
|
function getApiBase(): string {
|
|
59
|
-
const raw = getEnv("
|
|
58
|
+
const raw = getEnv("TUNNEL_API_URL") || FALLBACK_API_URL;
|
|
60
59
|
const url = raw.startsWith("http") ? raw : FALLBACK_API_URL;
|
|
61
60
|
return url.replace(/\/+$/, "");
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
const client = new TunnelClient({
|
|
65
64
|
apiUrl: `${getApiBase()}/v1/tunnel`,
|
|
66
|
-
token: getEnv("
|
|
67
|
-
tunnelId: getEnv("
|
|
65
|
+
token: getEnv("TUNNEL_TOKEN") || "",
|
|
66
|
+
tunnelId: getEnv("TUNNEL_ID"),
|
|
68
67
|
});
|
|
69
68
|
|
|
70
69
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
@@ -619,7 +618,7 @@ const [cmd, rawArgs] = process.argv.slice(2);
|
|
|
619
618
|
|
|
620
619
|
if (!cmd) {
|
|
621
620
|
console.error(
|
|
622
|
-
`Usage:
|
|
621
|
+
`Usage: agent-tunnel-cli <command> [args as JSON]\n\nAvailable: ${ALL_COMMANDS.join(" | ")}`
|
|
623
622
|
);
|
|
624
623
|
process.exit(1);
|
|
625
624
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket polyfill for Node.js < 22.
|
|
3
|
+
* If global WebSocket is already available (Node 22+, Bun, browsers), this is a no-op.
|
|
4
|
+
* Otherwise, it loads the `ws` package and assigns it to globalThis.WebSocket.
|
|
5
|
+
*/
|
|
6
|
+
if (typeof globalThis.WebSocket === 'undefined') {
|
|
7
|
+
try {
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
9
|
+
const ws = require('ws');
|
|
10
|
+
globalThis.WebSocket = ws.default || ws;
|
|
11
|
+
} catch {
|
|
12
|
+
console.error(
|
|
13
|
+
'[agent-tunnel] WebSocket is not available. Install the "ws" package or use Node.js 22+.',
|
|
14
|
+
);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/server/ws-handler.ts
CHANGED
|
@@ -81,6 +81,11 @@ export function createWsHandlers(relay: TunnelRelay, opts?: WsHandlerOptions): W
|
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// Send signing key to agent so it never needs the server secret
|
|
85
|
+
try {
|
|
86
|
+
pending.ws.send(JSON.stringify({ type: 'auth_ok', signingKey: result.signingKey }));
|
|
87
|
+
} catch {}
|
|
88
|
+
|
|
84
89
|
relay.registerAgent(tunnelId, pending.ws, result.signingKey, result.metadata);
|
|
85
90
|
if (heartbeat) {
|
|
86
91
|
heartbeat.register(tunnelId);
|
package/src/shared/crypto.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createHash, createHmac, timingSafeEqual, randomBytes } from 'crypto';
|
|
2
2
|
|
|
3
|
-
const SIGNING_KEY_CONTEXT = 'kortix-tunnel-signing-v1';
|
|
4
3
|
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
5
4
|
|
|
6
5
|
function randomAlphanumeric(length: number): string {
|
|
@@ -12,8 +11,8 @@ function randomAlphanumeric(length: number): string {
|
|
|
12
11
|
return result;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
export function deriveSigningKey(token: string): string {
|
|
16
|
-
return createHmac('sha256',
|
|
14
|
+
export function deriveSigningKey(token: string, secret: string): string {
|
|
15
|
+
return createHmac('sha256', secret)
|
|
17
16
|
.update(token)
|
|
18
17
|
.digest('hex');
|
|
19
18
|
}
|