@terminai/a2a-server 0.21.0
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 +5 -0
- package/dist/.last_build +0 -0
- package/dist/a2a-server.mjs +415698 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/src/agent/executor.d.ts +41 -0
- package/dist/src/agent/executor.js +408 -0
- package/dist/src/agent/executor.js.map +1 -0
- package/dist/src/agent/task.d.ts +67 -0
- package/dist/src/agent/task.js +799 -0
- package/dist/src/agent/task.js.map +1 -0
- package/dist/src/agent/task.test.d.ts +7 -0
- package/dist/src/agent/task.test.js +435 -0
- package/dist/src/agent/task.test.js.map +1 -0
- package/dist/src/agent/task.token.test.d.ts +7 -0
- package/dist/src/agent/task.token.test.js +53 -0
- package/dist/src/agent/task.token.test.js.map +1 -0
- package/dist/src/auth/llmAuthManager.d.ts +39 -0
- package/dist/src/auth/llmAuthManager.js +209 -0
- package/dist/src/auth/llmAuthManager.js.map +1 -0
- package/dist/src/auth/llmAuthManager.test.d.ts +7 -0
- package/dist/src/auth/llmAuthManager.test.js +92 -0
- package/dist/src/auth/llmAuthManager.test.js.map +1 -0
- package/dist/src/commands/command-registry.d.ts +16 -0
- package/dist/src/commands/command-registry.js +35 -0
- package/dist/src/commands/command-registry.js.map +1 -0
- package/dist/src/commands/command-registry.test.d.ts +7 -0
- package/dist/src/commands/command-registry.test.js +100 -0
- package/dist/src/commands/command-registry.test.js.map +1 -0
- package/dist/src/commands/extensions.d.ts +19 -0
- package/dist/src/commands/extensions.js +26 -0
- package/dist/src/commands/extensions.js.map +1 -0
- package/dist/src/commands/extensions.test.d.ts +7 -0
- package/dist/src/commands/extensions.test.js +70 -0
- package/dist/src/commands/extensions.test.js.map +1 -0
- package/dist/src/commands/init.d.ts +16 -0
- package/dist/src/commands/init.js +111 -0
- package/dist/src/commands/init.js.map +1 -0
- package/dist/src/commands/init.test.d.ts +7 -0
- package/dist/src/commands/init.test.js +146 -0
- package/dist/src/commands/init.test.js.map +1 -0
- package/dist/src/commands/restore.d.ts +21 -0
- package/dist/src/commands/restore.js +126 -0
- package/dist/src/commands/restore.js.map +1 -0
- package/dist/src/commands/restore.test.d.ts +7 -0
- package/dist/src/commands/restore.test.js +111 -0
- package/dist/src/commands/restore.test.js.map +1 -0
- package/dist/src/commands/types.d.ts +33 -0
- package/dist/src/commands/types.js +8 -0
- package/dist/src/commands/types.js.map +1 -0
- package/dist/src/config/config.d.ts +24 -0
- package/dist/src/config/config.js +140 -0
- package/dist/src/config/config.js.map +1 -0
- package/dist/src/config/extension.d.ts +12 -0
- package/dist/src/config/extension.js +105 -0
- package/dist/src/config/extension.js.map +1 -0
- package/dist/src/config/settings.d.ts +15 -0
- package/dist/src/config/settings.js +20 -0
- package/dist/src/config/settings.js.map +1 -0
- package/dist/src/config/settings.test.d.ts +7 -0
- package/dist/src/config/settings.test.js +170 -0
- package/dist/src/config/settings.test.js.map +1 -0
- package/dist/src/http/app.d.ts +17 -0
- package/dist/src/http/app.js +399 -0
- package/dist/src/http/app.js.map +1 -0
- package/dist/src/http/app.test.d.ts +7 -0
- package/dist/src/http/app.test.js +1048 -0
- package/dist/src/http/app.test.js.map +1 -0
- package/dist/src/http/auth.d.ts +21 -0
- package/dist/src/http/auth.js +55 -0
- package/dist/src/http/auth.js.map +1 -0
- package/dist/src/http/auth.test.d.ts +7 -0
- package/dist/src/http/auth.test.js +53 -0
- package/dist/src/http/auth.test.js.map +1 -0
- package/dist/src/http/authRoutes.test.d.ts +7 -0
- package/dist/src/http/authRoutes.test.js +169 -0
- package/dist/src/http/authRoutes.test.js.map +1 -0
- package/dist/src/http/cors.d.ts +8 -0
- package/dist/src/http/cors.js +96 -0
- package/dist/src/http/cors.js.map +1 -0
- package/dist/src/http/cors.test.d.ts +7 -0
- package/dist/src/http/cors.test.js +62 -0
- package/dist/src/http/cors.test.js.map +1 -0
- package/dist/src/http/deferredAuth.test.d.ts +7 -0
- package/dist/src/http/deferredAuth.test.js +45 -0
- package/dist/src/http/deferredAuth.test.js.map +1 -0
- package/dist/src/http/endpoints.test.d.ts +7 -0
- package/dist/src/http/endpoints.test.js +149 -0
- package/dist/src/http/endpoints.test.js.map +1 -0
- package/dist/src/http/llmAuthMiddleware.d.ts +9 -0
- package/dist/src/http/llmAuthMiddleware.js +37 -0
- package/dist/src/http/llmAuthMiddleware.js.map +1 -0
- package/dist/src/http/relay.d.ts +28 -0
- package/dist/src/http/relay.js +342 -0
- package/dist/src/http/relay.js.map +1 -0
- package/dist/src/http/relay.test.d.ts +7 -0
- package/dist/src/http/relay.test.js +149 -0
- package/dist/src/http/relay.test.js.map +1 -0
- package/dist/src/http/replay.d.ts +19 -0
- package/dist/src/http/replay.js +90 -0
- package/dist/src/http/replay.js.map +1 -0
- package/dist/src/http/replay.test.d.ts +7 -0
- package/dist/src/http/replay.test.js +78 -0
- package/dist/src/http/replay.test.js.map +1 -0
- package/dist/src/http/requestStorage.d.ts +11 -0
- package/dist/src/http/requestStorage.js +9 -0
- package/dist/src/http/requestStorage.js.map +1 -0
- package/dist/src/http/routes/auth.d.ts +9 -0
- package/dist/src/http/routes/auth.js +125 -0
- package/dist/src/http/routes/auth.js.map +1 -0
- package/dist/src/http/server.d.ts +8 -0
- package/dist/src/http/server.js +28 -0
- package/dist/src/http/server.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +11 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/persistence/gcs.d.ts +25 -0
- package/dist/src/persistence/gcs.js +248 -0
- package/dist/src/persistence/gcs.js.map +1 -0
- package/dist/src/persistence/gcs.test.d.ts +7 -0
- package/dist/src/persistence/gcs.test.js +335 -0
- package/dist/src/persistence/gcs.test.js.map +1 -0
- package/dist/src/persistence/remoteAuthStore.d.ts +21 -0
- package/dist/src/persistence/remoteAuthStore.js +74 -0
- package/dist/src/persistence/remoteAuthStore.js.map +1 -0
- package/dist/src/types.d.ts +100 -0
- package/dist/src/types.js +49 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/envAliases.d.ts +7 -0
- package/dist/src/utils/envAliases.js +9 -0
- package/dist/src/utils/envAliases.js.map +1 -0
- package/dist/src/utils/executor_utils.d.ts +8 -0
- package/dist/src/utils/executor_utils.js +42 -0
- package/dist/src/utils/executor_utils.js.map +1 -0
- package/dist/src/utils/logger.d.ts +9 -0
- package/dist/src/utils/logger.js +26 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/dist/src/utils/redactSecrets.d.ts +16 -0
- package/dist/src/utils/redactSecrets.js +72 -0
- package/dist/src/utils/redactSecrets.js.map +1 -0
- package/dist/src/utils/redactSecrets.test.d.ts +7 -0
- package/dist/src/utils/redactSecrets.test.js +62 -0
- package/dist/src/utils/redactSecrets.test.js.map +1 -0
- package/dist/src/utils/testing_utils.d.ts +48 -0
- package/dist/src/utils/testing_utils.js +173 -0
- package/dist/src/utils/testing_utils.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/web-client/app.js +526 -0
- package/dist/web-client/index.html +43 -0
- package/dist/web-client/package.json +10 -0
- package/dist/web-client/relay-client.js +330 -0
- package/dist/web-client/style.css +189 -0
- package/package.json +53 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* Portions Copyright 2025 TerminaI Authors
|
|
5
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
6
|
+
*/
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import { createMockConfig, TEST_REMOTE_TOKEN } from '../utils/testing_utils.js';
|
|
9
|
+
const loadConfigSpy = vi.hoisted(() => vi.fn());
|
|
10
|
+
vi.mock('../config/config.js', async (importOriginal) => {
|
|
11
|
+
const actual = await importOriginal();
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
loadConfig: loadConfigSpy,
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
describe('deferred auth default (Task 18)', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
process.env['NODE_ENV'] = 'test';
|
|
20
|
+
process.env['GEMINI_WEB_REMOTE_TOKEN'] = TEST_REMOTE_TOKEN;
|
|
21
|
+
delete process.env['TERMINAI_A2A_DEFER_AUTH'];
|
|
22
|
+
delete process.env['GEMINI_A2A_DEFER_AUTH'];
|
|
23
|
+
delete process.env['TERMINAI_SIDECAR'];
|
|
24
|
+
loadConfigSpy.mockResolvedValue(createMockConfig({
|
|
25
|
+
refreshAuth: vi.fn().mockResolvedValue(undefined),
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
getWebRemoteRelayUrl: vi.fn().mockReturnValue(undefined),
|
|
28
|
+
}));
|
|
29
|
+
});
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
delete process.env['GEMINI_WEB_REMOTE_TOKEN'];
|
|
32
|
+
delete process.env['TERMINAI_SIDECAR'];
|
|
33
|
+
vi.resetAllMocks();
|
|
34
|
+
});
|
|
35
|
+
it('enables deferLlmAuth when TERMINAI_SIDECAR=1', async () => {
|
|
36
|
+
process.env['TERMINAI_SIDECAR'] = '1';
|
|
37
|
+
const { createApp } = await import('./app.js');
|
|
38
|
+
await createApp();
|
|
39
|
+
// Signature: loadConfig(loadedSettings, extensionLoader, taskId, targetDir?, { deferLlmAuth })
|
|
40
|
+
const lastCall = loadConfigSpy.mock.calls.at(-1);
|
|
41
|
+
expect(lastCall?.[2]).toBe('a2a-server');
|
|
42
|
+
expect(lastCall?.[4]).toEqual({ deferLlmAuth: true });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=deferredAuth.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deferredAuth.test.js","sourceRoot":"","sources":["../../../src/http/deferredAuth.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAGhF,MAAM,aAAa,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;AAEhD,EAAE,CAAC,IAAI,CAAC,qBAAqB,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE;IACtD,MAAM,MAAM,GAAG,MAAM,cAAc,EAAwC,CAAC;IAC5E,OAAO;QACL,GAAG,MAAM;QACT,UAAU,EAAE,aAAa;KAC1B,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,iCAAiC,EAAE,GAAG,EAAE;IAC/C,UAAU,CAAC,GAAG,EAAE;QACd,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,MAAM,CAAC;QACjC,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,GAAG,iBAAiB,CAAC;QAE3D,OAAO,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;QAC9C,OAAO,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;QAC5C,OAAO,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QAEvC,aAAa,CAAC,iBAAiB,CAC7B,gBAAgB,CAAC;YACf,WAAW,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC;YACjD,8DAA8D;YAC9D,oBAAoB,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,SAAS,CAAC;SACzD,CAAW,CACb,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;QAC9C,OAAO,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QACvC,EAAE,CAAC,aAAa,EAAE,CAAC;IACrB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;QAC5D,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,GAAG,GAAG,CAAC;QACtC,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;QAE/C,MAAM,SAAS,EAAE,CAAC;QAElB,+FAA+F;QAC/F,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACjD,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACzC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* Portions Copyright 2025 TerminaI Authors
|
|
5
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
8
|
+
import request from 'supertest';
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import * as os from 'node:os';
|
|
12
|
+
import { createApp, updateCoderAgentCardUrl } from './app.js';
|
|
13
|
+
import { createAuthHeader, createMockConfig, createSignedHeaders, TEST_REMOTE_TOKEN, canListenOnLocalhost, listenOnLocalhost, closeServer, } from '../utils/testing_utils.js';
|
|
14
|
+
import { debugLogger } from '@terminai/core';
|
|
15
|
+
// Mock the logger to avoid polluting test output
|
|
16
|
+
// Comment out to help debug
|
|
17
|
+
vi.mock('../utils/logger.js', () => ({
|
|
18
|
+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
19
|
+
}));
|
|
20
|
+
// Mock Task.create to avoid its complex setup
|
|
21
|
+
vi.mock('../agent/task.js', () => {
|
|
22
|
+
class MockTask {
|
|
23
|
+
id;
|
|
24
|
+
contextId;
|
|
25
|
+
taskState = 'submitted';
|
|
26
|
+
config = {
|
|
27
|
+
getContentGeneratorConfig: vi
|
|
28
|
+
.fn()
|
|
29
|
+
.mockReturnValue({ model: 'gemini-pro' }),
|
|
30
|
+
};
|
|
31
|
+
geminiClient = {
|
|
32
|
+
initialize: vi.fn().mockResolvedValue(undefined),
|
|
33
|
+
};
|
|
34
|
+
constructor(id, contextId) {
|
|
35
|
+
this.id = id;
|
|
36
|
+
this.contextId = contextId;
|
|
37
|
+
}
|
|
38
|
+
static create = vi
|
|
39
|
+
.fn()
|
|
40
|
+
.mockImplementation((id, contextId) => Promise.resolve(new MockTask(id, contextId)));
|
|
41
|
+
getMetadata = vi.fn().mockImplementation(async () => ({
|
|
42
|
+
id: this.id,
|
|
43
|
+
contextId: this.contextId,
|
|
44
|
+
taskState: this.taskState,
|
|
45
|
+
model: 'gemini-pro',
|
|
46
|
+
mcpServers: [],
|
|
47
|
+
availableTools: [],
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
return { Task: MockTask };
|
|
51
|
+
});
|
|
52
|
+
vi.mock('../config/config.js', async () => {
|
|
53
|
+
const actual = await vi.importActual('../config/config.js');
|
|
54
|
+
return {
|
|
55
|
+
...actual,
|
|
56
|
+
loadConfig: vi
|
|
57
|
+
.fn()
|
|
58
|
+
.mockImplementation(async () => createMockConfig({})),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
const CAN_LISTEN = await canListenOnLocalhost();
|
|
62
|
+
const describeIfListen = CAN_LISTEN ? describe : describe.skip;
|
|
63
|
+
describeIfListen('Agent Server Endpoints', () => {
|
|
64
|
+
let app;
|
|
65
|
+
let server;
|
|
66
|
+
let testWorkspace;
|
|
67
|
+
const createTask = (contextId) => request(app)
|
|
68
|
+
.post('/tasks')
|
|
69
|
+
.set(createSignedHeaders('POST', '/tasks', {
|
|
70
|
+
contextId,
|
|
71
|
+
agentSettings: {
|
|
72
|
+
kind: 'agent-settings',
|
|
73
|
+
workspacePath: testWorkspace,
|
|
74
|
+
},
|
|
75
|
+
}))
|
|
76
|
+
.set('Content-Type', 'application/json')
|
|
77
|
+
.send({
|
|
78
|
+
contextId,
|
|
79
|
+
agentSettings: {
|
|
80
|
+
kind: 'agent-settings',
|
|
81
|
+
workspacePath: testWorkspace,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
beforeAll(async () => {
|
|
85
|
+
process.env['GEMINI_WEB_REMOTE_TOKEN'] = TEST_REMOTE_TOKEN;
|
|
86
|
+
// Create a unique temporary directory for the workspace to avoid conflicts
|
|
87
|
+
testWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-agent-test-'));
|
|
88
|
+
app = await createApp();
|
|
89
|
+
server = await listenOnLocalhost(app);
|
|
90
|
+
const port = server.address().port;
|
|
91
|
+
updateCoderAgentCardUrl(port);
|
|
92
|
+
});
|
|
93
|
+
afterAll(async () => {
|
|
94
|
+
if (server) {
|
|
95
|
+
await closeServer(server);
|
|
96
|
+
}
|
|
97
|
+
delete process.env['GEMINI_WEB_REMOTE_TOKEN'];
|
|
98
|
+
if (testWorkspace) {
|
|
99
|
+
try {
|
|
100
|
+
fs.rmSync(testWorkspace, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
debugLogger.warn(`Could not remove temp dir '${testWorkspace}':`, e);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
it('should create a new task via POST /tasks', async () => {
|
|
108
|
+
const response = await createTask('test-context');
|
|
109
|
+
expect(response.status).toBe(201);
|
|
110
|
+
expect(response.body).toBeTypeOf('string'); // Should return the task ID
|
|
111
|
+
}, 7000);
|
|
112
|
+
it('should get metadata for a specific task via GET /tasks/:taskId/metadata', async () => {
|
|
113
|
+
const createResponse = await createTask('test-context-2');
|
|
114
|
+
const taskId = createResponse.body;
|
|
115
|
+
const response = await request(app)
|
|
116
|
+
.get(`/tasks/${taskId}/metadata`)
|
|
117
|
+
.set(createAuthHeader());
|
|
118
|
+
expect(response.status).toBe(200);
|
|
119
|
+
expect(response.body.metadata.id).toBe(taskId);
|
|
120
|
+
}, 6000);
|
|
121
|
+
it('should get metadata for all tasks via GET /tasks/metadata', async () => {
|
|
122
|
+
const createResponse = await createTask('test-context-3');
|
|
123
|
+
const taskId = createResponse.body;
|
|
124
|
+
const response = await request(app)
|
|
125
|
+
.get('/tasks/metadata')
|
|
126
|
+
.set(createAuthHeader());
|
|
127
|
+
expect(response.status).toBe(200);
|
|
128
|
+
expect(Array.isArray(response.body)).toBe(true);
|
|
129
|
+
expect(response.body.length).toBeGreaterThan(0);
|
|
130
|
+
const taskMetadata = response.body.find((m) => m.id === taskId);
|
|
131
|
+
expect(taskMetadata).toBeDefined();
|
|
132
|
+
});
|
|
133
|
+
it('should return 404 for a non-existent task', async () => {
|
|
134
|
+
const response = await request(app)
|
|
135
|
+
.get('/tasks/fake-task/metadata')
|
|
136
|
+
.set(createAuthHeader());
|
|
137
|
+
expect(response.status).toBe(404);
|
|
138
|
+
});
|
|
139
|
+
it('should return agent metadata via GET /.well-known/agent-card.json', async () => {
|
|
140
|
+
const response = await request(app)
|
|
141
|
+
.get('/.well-known/agent-card.json')
|
|
142
|
+
.set(createAuthHeader());
|
|
143
|
+
const port = server.address().port;
|
|
144
|
+
expect(response.status).toBe(200);
|
|
145
|
+
expect(response.body.name).toBe('Gemini SDLC Agent');
|
|
146
|
+
expect(response.body.url).toBe(`http://localhost:${port}/`);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
//# sourceMappingURL=endpoints.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"endpoints.test.js","sourceRoot":"","sources":["../../../src/http/endpoints.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* Portions Copyright 2025 TerminaI Authors
|
|
5
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
6
|
+
*/
|
|
7
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
8
|
+
import { LlmAuthManager } from '../auth/llmAuthManager.js';
|
|
9
|
+
export declare function createLlmAuthMiddleware(authManager: LlmAuthManager): (_req: Request, res: Response, next: NextFunction) => void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* Portions Copyright 2025 TerminaI Authors
|
|
5
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
6
|
+
*/
|
|
7
|
+
import { LlmAuthManager } from '../auth/llmAuthManager.js';
|
|
8
|
+
export function createLlmAuthMiddleware(authManager) {
|
|
9
|
+
return (_req, res, next) => {
|
|
10
|
+
void authManager
|
|
11
|
+
.getStatus()
|
|
12
|
+
.then((check) => {
|
|
13
|
+
if (check.status !== 'ok') {
|
|
14
|
+
res.status(503).json({
|
|
15
|
+
error: 'Authentication required',
|
|
16
|
+
code: 'AUTH_REQUIRED',
|
|
17
|
+
details: check,
|
|
18
|
+
});
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
next();
|
|
22
|
+
})
|
|
23
|
+
.catch((err) => {
|
|
24
|
+
// Fail closed: if status check fails, treat as auth required.
|
|
25
|
+
res.status(503).json({
|
|
26
|
+
error: 'Authentication required',
|
|
27
|
+
code: 'AUTH_REQUIRED',
|
|
28
|
+
details: {
|
|
29
|
+
status: 'error',
|
|
30
|
+
authType: null,
|
|
31
|
+
message: err instanceof Error ? err.message : 'Auth status check failed',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=llmAuthMiddleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llmAuthMiddleware.js","sourceRoot":"","sources":["../../../src/http/llmAuthMiddleware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAE3D,MAAM,UAAU,uBAAuB,CAAC,WAA2B;IACjE,OAAO,CAAC,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QAC1D,KAAK,WAAW;aACb,SAAS,EAAE;aACX,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;YACd,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;gBAC1B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBACnB,KAAK,EAAE,yBAAyB;oBAChC,IAAI,EAAE,eAAe;oBACrB,OAAO,EAAE,KAAK;iBACf,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YACD,IAAI,EAAE,CAAC;QACT,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACb,8DAA8D;YAC9D,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,KAAK,EAAE,yBAAyB;gBAChC,IAAI,EAAE,eAAe;gBACrB,OAAO,EAAE;oBACP,MAAM,EAAE,OAAO;oBACf,QAAQ,EAAE,IAAI;oBACd,OAAO,EACL,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,0BAA0B;iBAClE;aACF,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* Portions Copyright 2025 TerminaI Authors
|
|
5
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
6
|
+
*/
|
|
7
|
+
import type { DefaultRequestHandler } from '@a2a-js/sdk/server';
|
|
8
|
+
export type RelayEnvelope = {
|
|
9
|
+
v: 1 | 2;
|
|
10
|
+
type: 'HELLO' | 'HELLO_ACK' | 'PAIR' | 'PAIR_ACK' | 'RPC' | 'EVENT' | 'ERROR' | 'PING' | 'PONG' | 'CLOSE';
|
|
11
|
+
dir: 'c2h' | 'h2c';
|
|
12
|
+
seq: number;
|
|
13
|
+
ts: number;
|
|
14
|
+
epoch?: string;
|
|
15
|
+
payload: unknown;
|
|
16
|
+
};
|
|
17
|
+
export type RelayEnvelopeV1 = RelayEnvelope;
|
|
18
|
+
export interface RelaySession {
|
|
19
|
+
sessionId: string;
|
|
20
|
+
key: Buffer;
|
|
21
|
+
shareUrl: string;
|
|
22
|
+
reconnectAttempts: number;
|
|
23
|
+
pairingRequired: boolean;
|
|
24
|
+
pairingCode?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function createRelaySession(relayUrl: string): RelaySession;
|
|
27
|
+
export declare function connectToRelay(relayUrl: string, requestHandler: DefaultRequestHandler): Promise<void>;
|
|
28
|
+
export declare function runRelayConnection(session: RelaySession, relayUrl: string, requestHandler: DefaultRequestHandler): Promise<void>;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* Portions Copyright 2025 TerminaI Authors
|
|
5
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
6
|
+
*/
|
|
7
|
+
import { WebSocket } from 'ws';
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
10
|
+
import { logger } from '../utils/logger.js';
|
|
11
|
+
// Protocol version constants
|
|
12
|
+
const PROTOCOL_VERSIONS = {
|
|
13
|
+
V1: 1,
|
|
14
|
+
V2: 2,
|
|
15
|
+
};
|
|
16
|
+
// Allow v1 fallback via environment variable (for transitional deployments)
|
|
17
|
+
const ALLOW_INSECURE_V1 = process.env['ALLOW_INSECURE_RELAY_V1'] === 'true';
|
|
18
|
+
export function createRelaySession(relayUrl) {
|
|
19
|
+
const sessionId = uuidv4();
|
|
20
|
+
// Generate 256-bit key for AES-GCM
|
|
21
|
+
const key = crypto.randomBytes(32);
|
|
22
|
+
const keyBase64 = key.toString('base64');
|
|
23
|
+
// Construct user-friendly URL (Key is in hash, so it's never sent to server)
|
|
24
|
+
// Default published Web Client URL: https://terminai.org/remote
|
|
25
|
+
const webClientUrl = process.env['GEMINI_WEB_CLIENT_URL'] || 'https://terminai.org/remote';
|
|
26
|
+
const pairingRequired = true; // Always require pairing for security
|
|
27
|
+
const pairingCode = Math.floor(100000 + Math.random() * 900000).toString(); // 6-digit code
|
|
28
|
+
const shareUrl = `${webClientUrl}#session=${sessionId}&key=${encodeURIComponent(keyBase64)}&relay=${encodeURIComponent(relayUrl)}`;
|
|
29
|
+
const printUrl = process.env['PRINT_RELAY_URL'] === 'true';
|
|
30
|
+
if (printUrl) {
|
|
31
|
+
logger.info(`[Relay] Remote Access URL: ${shareUrl}`);
|
|
32
|
+
logger.info('[Relay] (Share this URL securely. The key is in the hash and never verified by the server)');
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
logger.info(JSON.stringify({
|
|
36
|
+
event: 'session_created',
|
|
37
|
+
sessionIdHash: sessionId.slice(0, 8),
|
|
38
|
+
pairingCodeRequired: pairingRequired,
|
|
39
|
+
timestamp: Date.now(),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
logger.info(`[Relay] Pairing Code: ${pairingCode} (required for first connection)`);
|
|
43
|
+
return {
|
|
44
|
+
sessionId,
|
|
45
|
+
key,
|
|
46
|
+
shareUrl,
|
|
47
|
+
reconnectAttempts: 0,
|
|
48
|
+
pairingRequired,
|
|
49
|
+
pairingCode,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export async function connectToRelay(relayUrl, requestHandler) {
|
|
53
|
+
const session = createRelaySession(relayUrl);
|
|
54
|
+
return runRelayConnection(session, relayUrl, requestHandler);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build AAD string based on protocol version
|
|
58
|
+
*/
|
|
59
|
+
function buildAad(sessionId, dir, version, epoch) {
|
|
60
|
+
if (version === 2 && epoch) {
|
|
61
|
+
return `terminai-relay|v=2|session=${sessionId}|epoch=${epoch}|dir=${dir}`;
|
|
62
|
+
}
|
|
63
|
+
return `terminai-relay|v=1|session=${sessionId}|dir=${dir}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Encrypt an envelope for sending to client
|
|
67
|
+
*/
|
|
68
|
+
function encryptEnvelope(envelope, key, sessionId, version, epoch) {
|
|
69
|
+
const envelopeBuffer = Buffer.from(JSON.stringify(envelope), 'utf8');
|
|
70
|
+
const iv = crypto.randomBytes(12);
|
|
71
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
72
|
+
const aad = buildAad(sessionId, 'h2c', version, epoch);
|
|
73
|
+
cipher.setAAD(Buffer.from(aad, 'utf8'));
|
|
74
|
+
let ciphertext = cipher.update(envelopeBuffer);
|
|
75
|
+
ciphertext = Buffer.concat([ciphertext, cipher.final()]);
|
|
76
|
+
const tag = cipher.getAuthTag();
|
|
77
|
+
return Buffer.concat([iv, tag, ciphertext]);
|
|
78
|
+
}
|
|
79
|
+
export async function runRelayConnection(session, relayUrl, requestHandler) {
|
|
80
|
+
// Create new connection state with fresh epoch
|
|
81
|
+
const connState = {
|
|
82
|
+
inboundMaxSeq: 0,
|
|
83
|
+
outboundSeq: 0,
|
|
84
|
+
handshakeState: 'WAIT_HELLO',
|
|
85
|
+
protocolVersion: 2, // Default to v2, will be negotiated in handshake
|
|
86
|
+
epoch: crypto.randomBytes(8).toString('hex'), // New epoch per connection
|
|
87
|
+
};
|
|
88
|
+
logger.info(JSON.stringify({
|
|
89
|
+
event: 'relay_connect_attempt',
|
|
90
|
+
sessionIdHash: session.sessionId.slice(0, 8),
|
|
91
|
+
epoch: connState.epoch.slice(0, 8),
|
|
92
|
+
timestamp: Date.now(),
|
|
93
|
+
}));
|
|
94
|
+
const ws = new WebSocket(`${relayUrl}?role=host&session=${session.sessionId}`);
|
|
95
|
+
ws.on('open', () => {
|
|
96
|
+
logger.info(JSON.stringify({
|
|
97
|
+
event: 'relay_connected',
|
|
98
|
+
sessionIdHash: session.sessionId.slice(0, 8),
|
|
99
|
+
timestamp: Date.now(),
|
|
100
|
+
}));
|
|
101
|
+
session.reconnectAttempts = 0;
|
|
102
|
+
});
|
|
103
|
+
ws.on('message', async (data) => {
|
|
104
|
+
try {
|
|
105
|
+
// Handle relay control messages (unencrypted JSON strings)
|
|
106
|
+
if (typeof data === 'string') {
|
|
107
|
+
try {
|
|
108
|
+
const ctrl = JSON.parse(data);
|
|
109
|
+
if (ctrl.type === 'RELAY_STATUS') {
|
|
110
|
+
logger.info(JSON.stringify({
|
|
111
|
+
event: 'relay_status',
|
|
112
|
+
status: ctrl.status,
|
|
113
|
+
sessionIdHash: session.sessionId.slice(0, 8),
|
|
114
|
+
timestamp: Date.now(),
|
|
115
|
+
}));
|
|
116
|
+
// No action needed for now - client handles reconnect
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Not JSON, silently ignore
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Encrypted messages must be Buffer
|
|
125
|
+
if (!Buffer.isBuffer(data)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const iv = data.subarray(0, 12);
|
|
129
|
+
const ciphertext = data.subarray(12);
|
|
130
|
+
const tag = ciphertext.subarray(0, 16);
|
|
131
|
+
const actualCiphertext = ciphertext.subarray(16);
|
|
132
|
+
// For HELLO, we need to try both v1 and v2 AAD since we don't know client version yet
|
|
133
|
+
let envelope;
|
|
134
|
+
let usedVersion = 2;
|
|
135
|
+
// Try v2 AAD first (with epoch), then v1
|
|
136
|
+
const aadV2 = buildAad(session.sessionId, 'c2h', 2, connState.epoch);
|
|
137
|
+
const aadV1 = buildAad(session.sessionId, 'c2h', 1);
|
|
138
|
+
for (const { aad, version } of [
|
|
139
|
+
{ aad: aadV2, version: 2 },
|
|
140
|
+
{ aad: aadV1, version: 1 },
|
|
141
|
+
]) {
|
|
142
|
+
try {
|
|
143
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', session.key, iv);
|
|
144
|
+
decipher.setAAD(Buffer.from(aad, 'utf8'));
|
|
145
|
+
decipher.setAuthTag(tag);
|
|
146
|
+
let decrypted = decipher.update(actualCiphertext);
|
|
147
|
+
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
148
|
+
envelope = JSON.parse(decrypted.toString('utf8'));
|
|
149
|
+
usedVersion = version;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Try next AAD
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!envelope) {
|
|
158
|
+
logger.warn(JSON.stringify({
|
|
159
|
+
event: 'decrypt_failed',
|
|
160
|
+
sessionIdHash: session.sessionId.slice(0, 8),
|
|
161
|
+
timestamp: Date.now(),
|
|
162
|
+
}));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Validate envelope sequence
|
|
166
|
+
if (envelope.dir !== 'c2h' ||
|
|
167
|
+
envelope.seq !== connState.inboundMaxSeq + 1) {
|
|
168
|
+
logger.warn(JSON.stringify({
|
|
169
|
+
event: 'invalid_envelope',
|
|
170
|
+
reason: envelope.seq !== connState.inboundMaxSeq + 1
|
|
171
|
+
? 'seq_mismatch'
|
|
172
|
+
: 'invalid_dir',
|
|
173
|
+
expected: connState.inboundMaxSeq + 1,
|
|
174
|
+
got: envelope.seq,
|
|
175
|
+
sessionIdHash: session.sessionId.slice(0, 8),
|
|
176
|
+
timestamp: Date.now(),
|
|
177
|
+
}));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
connState.inboundMaxSeq = envelope.seq;
|
|
181
|
+
if (envelope.type === 'HELLO') {
|
|
182
|
+
// Version negotiation
|
|
183
|
+
const clientProtocols = envelope.payload
|
|
184
|
+
.protocols || [1];
|
|
185
|
+
const supportedVersions = ALLOW_INSECURE_V1
|
|
186
|
+
? [PROTOCOL_VERSIONS.V2, PROTOCOL_VERSIONS.V1]
|
|
187
|
+
: [PROTOCOL_VERSIONS.V2];
|
|
188
|
+
const selectedVersion = supportedVersions.find((v) => clientProtocols.includes(v));
|
|
189
|
+
if (!selectedVersion) {
|
|
190
|
+
// Client too old, send error
|
|
191
|
+
const errorEnvelope = {
|
|
192
|
+
v: usedVersion,
|
|
193
|
+
type: 'ERROR',
|
|
194
|
+
dir: 'h2c',
|
|
195
|
+
seq: ++connState.outboundSeq,
|
|
196
|
+
ts: Date.now(),
|
|
197
|
+
payload: {
|
|
198
|
+
code: 'VERSION_MISMATCH',
|
|
199
|
+
message: 'Client too old, update required. Server requires protocol v2.',
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
const errorPayload = encryptEnvelope(errorEnvelope, session.key, session.sessionId, usedVersion, connState.epoch);
|
|
203
|
+
ws.send(errorPayload);
|
|
204
|
+
ws.close(1002, 'Protocol version mismatch');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
connState.protocolVersion = selectedVersion;
|
|
208
|
+
connState.handshakeState = 'READY';
|
|
209
|
+
const ackEnvelope = {
|
|
210
|
+
v: selectedVersion,
|
|
211
|
+
type: 'HELLO_ACK',
|
|
212
|
+
dir: 'h2c',
|
|
213
|
+
seq: ++connState.outboundSeq,
|
|
214
|
+
ts: Date.now(),
|
|
215
|
+
epoch: selectedVersion === 2 ? connState.epoch : undefined,
|
|
216
|
+
payload: {
|
|
217
|
+
selectedVersion,
|
|
218
|
+
requiresPairing: session.pairingRequired,
|
|
219
|
+
...(selectedVersion === 2 ? { epoch: connState.epoch } : {}),
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
const ackPayload = encryptEnvelope(ackEnvelope, session.key, session.sessionId, selectedVersion, selectedVersion === 2 ? connState.epoch : undefined);
|
|
223
|
+
ws.send(ackPayload);
|
|
224
|
+
logger.info(JSON.stringify({
|
|
225
|
+
event: 'handshake_complete',
|
|
226
|
+
protocolVersion: selectedVersion,
|
|
227
|
+
sessionIdHash: session.sessionId.slice(0, 8),
|
|
228
|
+
timestamp: Date.now(),
|
|
229
|
+
}));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (connState.handshakeState !== 'READY') {
|
|
233
|
+
logger.warn(JSON.stringify({
|
|
234
|
+
event: 'message_before_handshake',
|
|
235
|
+
sessionIdHash: session.sessionId.slice(0, 8),
|
|
236
|
+
timestamp: Date.now(),
|
|
237
|
+
}));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// For v2, validate epoch in subsequent messages
|
|
241
|
+
if (connState.protocolVersion === 2 &&
|
|
242
|
+
envelope.epoch !== connState.epoch) {
|
|
243
|
+
logger.warn(JSON.stringify({
|
|
244
|
+
event: 'epoch_mismatch',
|
|
245
|
+
sessionIdHash: session.sessionId.slice(0, 8),
|
|
246
|
+
timestamp: Date.now(),
|
|
247
|
+
}));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (envelope.type === 'PAIR') {
|
|
251
|
+
const code = envelope.payload.code;
|
|
252
|
+
const success = code === session.pairingCode;
|
|
253
|
+
if (success) {
|
|
254
|
+
session.pairingRequired = false;
|
|
255
|
+
logger.info(JSON.stringify({
|
|
256
|
+
event: 'pairing_success',
|
|
257
|
+
sessionIdHash: session.sessionId.slice(0, 8),
|
|
258
|
+
timestamp: Date.now(),
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
logger.warn(JSON.stringify({
|
|
263
|
+
event: 'pairing_failure',
|
|
264
|
+
sessionIdHash: session.sessionId.slice(0, 8),
|
|
265
|
+
timestamp: Date.now(),
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
// Send pairing result back to client
|
|
269
|
+
const pairResultEnvelope = {
|
|
270
|
+
v: connState.protocolVersion,
|
|
271
|
+
type: success ? 'PAIR_ACK' : 'ERROR',
|
|
272
|
+
dir: 'h2c',
|
|
273
|
+
seq: ++connState.outboundSeq,
|
|
274
|
+
ts: Date.now(),
|
|
275
|
+
epoch: connState.protocolVersion === 2 ? connState.epoch : undefined,
|
|
276
|
+
payload: {
|
|
277
|
+
success,
|
|
278
|
+
message: success ? 'Paired successfully' : 'Invalid pairing code',
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
const pairResultPayload = encryptEnvelope(pairResultEnvelope, session.key, session.sessionId, connState.protocolVersion, connState.protocolVersion === 2 ? connState.epoch : undefined);
|
|
282
|
+
ws.send(pairResultPayload);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (envelope.type === 'RPC' && !session.pairingRequired) {
|
|
286
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
287
|
+
const result = await requestHandler.handle(envelope.payload);
|
|
288
|
+
let response;
|
|
289
|
+
if (result && typeof result[Symbol.asyncIterator] === 'function') {
|
|
290
|
+
const responses = [];
|
|
291
|
+
for await (const chunk of result) {
|
|
292
|
+
responses.push(chunk);
|
|
293
|
+
}
|
|
294
|
+
response = responses[responses.length - 1];
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
response = result;
|
|
298
|
+
}
|
|
299
|
+
// Encrypt Response
|
|
300
|
+
const respEnvelope = {
|
|
301
|
+
v: connState.protocolVersion,
|
|
302
|
+
type: 'RPC',
|
|
303
|
+
dir: 'h2c',
|
|
304
|
+
seq: ++connState.outboundSeq,
|
|
305
|
+
ts: Date.now(),
|
|
306
|
+
epoch: connState.protocolVersion === 2 ? connState.epoch : undefined,
|
|
307
|
+
payload: response,
|
|
308
|
+
};
|
|
309
|
+
const responsePayload = encryptEnvelope(respEnvelope, session.key, session.sessionId, connState.protocolVersion, connState.protocolVersion === 2 ? connState.epoch : undefined);
|
|
310
|
+
ws.send(responsePayload);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (e) {
|
|
314
|
+
logger.error(JSON.stringify({
|
|
315
|
+
event: 'relay_message_error',
|
|
316
|
+
sessionIdHash: session.sessionId.slice(0, 8),
|
|
317
|
+
error: e.message,
|
|
318
|
+
timestamp: Date.now(),
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
ws.on('error', (e) => {
|
|
323
|
+
logger.error(JSON.stringify({
|
|
324
|
+
event: 'relay_ws_error',
|
|
325
|
+
sessionIdHash: session.sessionId.slice(0, 8),
|
|
326
|
+
error: e.message,
|
|
327
|
+
timestamp: Date.now(),
|
|
328
|
+
}));
|
|
329
|
+
});
|
|
330
|
+
ws.on('close', () => {
|
|
331
|
+
session.reconnectAttempts++;
|
|
332
|
+
const delay = Math.min(5000 * Math.pow(2, session.reconnectAttempts - 1), 60000);
|
|
333
|
+
logger.warn(JSON.stringify({
|
|
334
|
+
event: 'relay_disconnected',
|
|
335
|
+
sessionIdHash: session.sessionId.slice(0, 8),
|
|
336
|
+
retryDelay: delay,
|
|
337
|
+
timestamp: Date.now(),
|
|
338
|
+
}));
|
|
339
|
+
setTimeout(() => runRelayConnection(session, relayUrl, requestHandler), delay);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
//# sourceMappingURL=relay.js.map
|