@itphx/synth-core 0.3.0 → 0.4.4
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/auth/local.d.ts +14 -0
- package/dist/auth/local.d.ts.map +1 -1
- package/dist/auth/local.js +63 -11
- package/dist/auth/local.js.map +1 -1
- package/dist/auth/local.test.js +59 -0
- package/dist/auth/local.test.js.map +1 -1
- package/dist/events/bus.d.ts +7 -0
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +26 -3
- package/dist/events/bus.js.map +1 -1
- package/dist/events/bus.test.js +46 -0
- package/dist/events/bus.test.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/main.js +247 -8
- package/dist/main.js.map +1 -1
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +4 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/remote.d.ts +60 -0
- package/dist/providers/remote.d.ts.map +1 -0
- package/dist/providers/remote.js +394 -0
- package/dist/providers/remote.js.map +1 -0
- package/dist/providers/remote.test.d.ts +2 -0
- package/dist/providers/remote.test.d.ts.map +1 -0
- package/dist/providers/remote.test.js +258 -0
- package/dist/providers/remote.test.js.map +1 -0
- package/dist/remote/aggregate.d.ts +43 -0
- package/dist/remote/aggregate.d.ts.map +1 -0
- package/dist/remote/aggregate.js +84 -0
- package/dist/remote/aggregate.js.map +1 -0
- package/dist/remote/aggregate.test.d.ts +2 -0
- package/dist/remote/aggregate.test.d.ts.map +1 -0
- package/dist/remote/aggregate.test.js +120 -0
- package/dist/remote/aggregate.test.js.map +1 -0
- package/dist/remote/client.d.ts +142 -0
- package/dist/remote/client.d.ts.map +1 -0
- package/dist/remote/client.js +290 -0
- package/dist/remote/client.js.map +1 -0
- package/dist/remote/client.test.d.ts +2 -0
- package/dist/remote/client.test.d.ts.map +1 -0
- package/dist/remote/client.test.js +176 -0
- package/dist/remote/client.test.js.map +1 -0
- package/dist/remote/connection.d.ts +66 -0
- package/dist/remote/connection.d.ts.map +1 -0
- package/dist/remote/connection.js +298 -0
- package/dist/remote/connection.js.map +1 -0
- package/dist/remote/connection.test.d.ts +2 -0
- package/dist/remote/connection.test.d.ts.map +1 -0
- package/dist/remote/connection.test.js +323 -0
- package/dist/remote/connection.test.js.map +1 -0
- package/dist/remote/credentials.d.ts +13 -0
- package/dist/remote/credentials.d.ts.map +1 -0
- package/dist/remote/credentials.js +81 -0
- package/dist/remote/credentials.js.map +1 -0
- package/dist/remote/credentials.test.d.ts +2 -0
- package/dist/remote/credentials.test.d.ts.map +1 -0
- package/dist/remote/credentials.test.js +69 -0
- package/dist/remote/credentials.test.js.map +1 -0
- package/dist/remote/device-flow.d.ts +72 -0
- package/dist/remote/device-flow.d.ts.map +1 -0
- package/dist/remote/device-flow.js +229 -0
- package/dist/remote/device-flow.js.map +1 -0
- package/dist/remote/device-flow.test.d.ts +2 -0
- package/dist/remote/device-flow.test.d.ts.map +1 -0
- package/dist/remote/device-flow.test.js +103 -0
- package/dist/remote/device-flow.test.js.map +1 -0
- package/dist/remote/event-bridge.d.ts +46 -0
- package/dist/remote/event-bridge.d.ts.map +1 -0
- package/dist/remote/event-bridge.js +218 -0
- package/dist/remote/event-bridge.js.map +1 -0
- package/dist/remote/event-bridge.test.d.ts +2 -0
- package/dist/remote/event-bridge.test.d.ts.map +1 -0
- package/dist/remote/event-bridge.test.js +240 -0
- package/dist/remote/event-bridge.test.js.map +1 -0
- package/dist/remote/index.d.ts +19 -0
- package/dist/remote/index.d.ts.map +1 -0
- package/dist/remote/index.js +11 -0
- package/dist/remote/index.js.map +1 -0
- package/dist/remote/session.d.ts +72 -0
- package/dist/remote/session.d.ts.map +1 -0
- package/dist/remote/session.js +248 -0
- package/dist/remote/session.js.map +1 -0
- package/dist/remote/session.test.d.ts +2 -0
- package/dist/remote/session.test.d.ts.map +1 -0
- package/dist/remote/session.test.js +482 -0
- package/dist/remote/session.test.js.map +1 -0
- package/dist/remote/transfer.d.ts +75 -0
- package/dist/remote/transfer.d.ts.map +1 -0
- package/dist/remote/transfer.js +491 -0
- package/dist/remote/transfer.js.map +1 -0
- package/dist/remote/transfer.test.d.ts +2 -0
- package/dist/remote/transfer.test.d.ts.map +1 -0
- package/dist/remote/transfer.test.js +416 -0
- package/dist/remote/transfer.test.js.map +1 -0
- package/dist/remote/types.d.ts +28 -0
- package/dist/remote/types.d.ts.map +1 -0
- package/dist/remote/types.js +10 -0
- package/dist/remote/types.js.map +1 -0
- package/dist/server/app.d.ts +62 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +364 -20
- package/dist/server/app.js.map +1 -1
- package/dist/server/app.proxy.test.d.ts +2 -0
- package/dist/server/app.proxy.test.d.ts.map +1 -0
- package/dist/server/app.proxy.test.js +193 -0
- package/dist/server/app.proxy.test.js.map +1 -0
- package/dist/server/app.remote.test.d.ts +2 -0
- package/dist/server/app.remote.test.d.ts.map +1 -0
- package/dist/server/app.remote.test.js +674 -0
- package/dist/server/app.remote.test.js.map +1 -0
- package/dist/server/proxy.d.ts +14 -0
- package/dist/server/proxy.d.ts.map +1 -0
- package/dist/server/proxy.js +93 -0
- package/dist/server/proxy.js.map +1 -0
- package/dist/server/routes/auth.d.ts +26 -2
- package/dist/server/routes/auth.d.ts.map +1 -1
- package/dist/server/routes/auth.js +269 -36
- package/dist/server/routes/auth.js.map +1 -1
- package/dist/server/routes/auth.test.d.ts +2 -0
- package/dist/server/routes/auth.test.d.ts.map +1 -0
- package/dist/server/routes/auth.test.js +466 -0
- package/dist/server/routes/auth.test.js.map +1 -0
- package/dist/server/routes/fs.d.ts +2 -1
- package/dist/server/routes/fs.d.ts.map +1 -1
- package/dist/server/routes/fs.js +159 -5
- package/dist/server/routes/fs.js.map +1 -1
- package/dist/server/routes/hybrid.d.ts +32 -0
- package/dist/server/routes/hybrid.d.ts.map +1 -0
- package/dist/server/routes/hybrid.js +21 -0
- package/dist/server/routes/hybrid.js.map +1 -0
- package/dist/server/routes/hybrid.test.d.ts +2 -0
- package/dist/server/routes/hybrid.test.d.ts.map +1 -0
- package/dist/server/routes/hybrid.test.js +454 -0
- package/dist/server/routes/hybrid.test.js.map +1 -0
- package/dist/server/routes/messages.d.ts +2 -1
- package/dist/server/routes/messages.d.ts.map +1 -1
- package/dist/server/routes/messages.js +104 -1
- package/dist/server/routes/messages.js.map +1 -1
- package/dist/server/routes/models.cache.test.d.ts +2 -0
- package/dist/server/routes/models.cache.test.d.ts.map +1 -0
- package/dist/server/routes/models.cache.test.js +59 -0
- package/dist/server/routes/models.cache.test.js.map +1 -0
- package/dist/server/routes/models.d.ts +9 -1
- package/dist/server/routes/models.d.ts.map +1 -1
- package/dist/server/routes/models.js +35 -2
- package/dist/server/routes/models.js.map +1 -1
- package/dist/server/routes/models.remote.test.d.ts +2 -0
- package/dist/server/routes/models.remote.test.d.ts.map +1 -0
- package/dist/server/routes/models.remote.test.js +101 -0
- package/dist/server/routes/models.remote.test.js.map +1 -0
- package/dist/server/routes/project-transfer.d.ts +16 -0
- package/dist/server/routes/project-transfer.d.ts.map +1 -0
- package/dist/server/routes/project-transfer.js +141 -0
- package/dist/server/routes/project-transfer.js.map +1 -0
- package/dist/server/routes/project-transfer.test.d.ts +2 -0
- package/dist/server/routes/project-transfer.test.d.ts.map +1 -0
- package/dist/server/routes/project-transfer.test.js +199 -0
- package/dist/server/routes/project-transfer.test.js.map +1 -0
- package/dist/server/routes/projects.d.ts +2 -1
- package/dist/server/routes/projects.d.ts.map +1 -1
- package/dist/server/routes/projects.js +101 -5
- package/dist/server/routes/projects.js.map +1 -1
- package/dist/server/routes/providers.d.ts +1 -1
- package/dist/server/routes/providers.d.ts.map +1 -1
- package/dist/server/routes/providers.js +3 -1
- package/dist/server/routes/providers.js.map +1 -1
- package/dist/server/routes/providers.test.d.ts +2 -0
- package/dist/server/routes/providers.test.d.ts.map +1 -0
- package/dist/server/routes/providers.test.js +58 -0
- package/dist/server/routes/providers.test.js.map +1 -0
- package/dist/server/routes/queue.d.ts +2 -1
- package/dist/server/routes/queue.d.ts.map +1 -1
- package/dist/server/routes/queue.js +89 -1
- package/dist/server/routes/queue.js.map +1 -1
- package/dist/server/routes/sessions.d.ts +8 -1
- package/dist/server/routes/sessions.d.ts.map +1 -1
- package/dist/server/routes/sessions.js +202 -9
- package/dist/server/routes/sessions.js.map +1 -1
- package/dist/server/serialize.d.ts +2 -0
- package/dist/server/serialize.d.ts.map +1 -1
- package/dist/server/serialize.js +2 -0
- package/dist/server/serialize.js.map +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-bridge.test.d.ts","sourceRoot":"","sources":["../../src/remote/event-bridge.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { EventBus } from '../events/bus.js';
|
|
3
|
+
import { RemoteEventBridge } from './event-bridge.js';
|
|
4
|
+
/** Response с SSE-блоками (каждый блок заканчивается \n\n). */
|
|
5
|
+
function sseResponse(blocks) {
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const stream = new ReadableStream({
|
|
8
|
+
start(controller) {
|
|
9
|
+
for (const b of blocks)
|
|
10
|
+
controller.enqueue(encoder.encode(b));
|
|
11
|
+
controller.close();
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
return new Response(stream, {
|
|
15
|
+
status: 200,
|
|
16
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/** Response с телом-стримом, который немедленно закрывается (без событий). */
|
|
20
|
+
function closedResponse() {
|
|
21
|
+
return new Response(new ReadableStream({ start(c) { c.close(); } }), { status: 200 });
|
|
22
|
+
}
|
|
23
|
+
/** Response с телом-стримом, который падает при чтении (обрыв сети). */
|
|
24
|
+
function errorResponse() {
|
|
25
|
+
return new Response(new ReadableStream({
|
|
26
|
+
start(c) { c.error(new Error('network drop')); },
|
|
27
|
+
}), { status: 200 });
|
|
28
|
+
}
|
|
29
|
+
function makeClient(request) {
|
|
30
|
+
return { request };
|
|
31
|
+
}
|
|
32
|
+
/** Флеш микрозадач (для тестов с fake timers). */
|
|
33
|
+
async function flush() {
|
|
34
|
+
for (let i = 0; i < 20; i++)
|
|
35
|
+
await Promise.resolve();
|
|
36
|
+
}
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
vi.useRealTimers();
|
|
39
|
+
});
|
|
40
|
+
describe('RemoteEventBridge', () => {
|
|
41
|
+
it('публикует события в eventBus с remote:<id> и переписанными вложенными id', async () => {
|
|
42
|
+
const bus = new EventBus();
|
|
43
|
+
const received = [];
|
|
44
|
+
bus.subscribeAll((e) => received.push(e));
|
|
45
|
+
const client = makeClient(vi.fn().mockResolvedValue(sseResponse([
|
|
46
|
+
'data: {"type":"stream.chunk","sessionId":"ses-1","data":{"stream_id":"st1","content":"привет"},"timestamp":"t1"}\n\n',
|
|
47
|
+
'data: {"type":"session.updated","sessionId":"ses-1","data":{"id":"ses-1","project_id":"prj-1","parent_id":"ses-0"},"timestamp":"t2"}\n\n',
|
|
48
|
+
'data: {"type":"subagent.resume.started","sessionId":"ses-1","data":{"child_session_id":"ses-2","parent_session_id":"ses-1"},"timestamp":"t3"}\n\n',
|
|
49
|
+
])));
|
|
50
|
+
const bridge = new RemoteEventBridge({
|
|
51
|
+
client,
|
|
52
|
+
getToken: async () => 'tok',
|
|
53
|
+
eventBus: bus,
|
|
54
|
+
backendUrl: 'http://backend.test',
|
|
55
|
+
});
|
|
56
|
+
bridge.start();
|
|
57
|
+
await vi.waitFor(() => expect(received.length).toBe(3));
|
|
58
|
+
expect(received[0]).toMatchObject({ type: 'stream.chunk', sessionId: 'remote:ses-1' });
|
|
59
|
+
expect(received[1]).toMatchObject({ type: 'session.updated', sessionId: 'remote:ses-1' });
|
|
60
|
+
expect(received[1].data.id).toBe('remote:ses-1');
|
|
61
|
+
expect(received[1].data.project_id).toBe('remote:prj-1');
|
|
62
|
+
expect(received[1].data.parent_id).toBe('remote:ses-0');
|
|
63
|
+
expect(received[2].data.child_session_id).toBe('remote:ses-2');
|
|
64
|
+
expect(received[2].data.parent_session_id).toBe('remote:ses-1');
|
|
65
|
+
// Повторный вызов не дублирует префикс (идемпотентность).
|
|
66
|
+
expect(received[1].data.id).toBe('remote:ses-1');
|
|
67
|
+
bridge.stop();
|
|
68
|
+
});
|
|
69
|
+
it('игнорирует keepalive-комментарии и невалидный JSON', async () => {
|
|
70
|
+
const bus = new EventBus();
|
|
71
|
+
const received = [];
|
|
72
|
+
bus.subscribeAll((e) => received.push(e));
|
|
73
|
+
const client = makeClient(vi.fn().mockResolvedValue(sseResponse([
|
|
74
|
+
':keepalive\n\n',
|
|
75
|
+
'data: {"type":"stream.start","sessionId":"ses-1","data":{"stream_id":"st1"},"timestamp":"t"}\n\n',
|
|
76
|
+
':keepalive\n\n',
|
|
77
|
+
'data: not-json\n\n',
|
|
78
|
+
])));
|
|
79
|
+
const bridge = new RemoteEventBridge({
|
|
80
|
+
client,
|
|
81
|
+
getToken: async () => 'tok',
|
|
82
|
+
eventBus: bus,
|
|
83
|
+
backendUrl: 'http://backend.test',
|
|
84
|
+
});
|
|
85
|
+
bridge.start();
|
|
86
|
+
await vi.waitFor(() => expect(received.length).toBe(1));
|
|
87
|
+
expect(received[0].type).toBe('stream.start');
|
|
88
|
+
expect(received[0].sessionId).toBe('remote:ses-1');
|
|
89
|
+
bridge.stop();
|
|
90
|
+
});
|
|
91
|
+
it('переподключается с backoff при разрыве стрима (fake timers)', async () => {
|
|
92
|
+
vi.useFakeTimers();
|
|
93
|
+
const bus = new EventBus();
|
|
94
|
+
const received = [];
|
|
95
|
+
bus.subscribeAll((e) => received.push(e));
|
|
96
|
+
// Первый стрим падает при чтении, второй — успешный с событием.
|
|
97
|
+
const request = vi.fn()
|
|
98
|
+
.mockResolvedValueOnce(errorResponse())
|
|
99
|
+
.mockResolvedValueOnce(sseResponse([
|
|
100
|
+
'data: {"type":"stream.chunk","sessionId":"ses-1","data":{"stream_id":"st1","content":"ok"},"timestamp":"t"}\n\n',
|
|
101
|
+
]));
|
|
102
|
+
const client = makeClient(request);
|
|
103
|
+
const bridge = new RemoteEventBridge({
|
|
104
|
+
client,
|
|
105
|
+
getToken: async () => 'tok',
|
|
106
|
+
eventBus: bus,
|
|
107
|
+
backendUrl: 'http://backend.test',
|
|
108
|
+
reconnectDelayMs: 2000,
|
|
109
|
+
maxReconnectDelayMs: 10000,
|
|
110
|
+
});
|
|
111
|
+
bridge.start();
|
|
112
|
+
// Дожидаемся первого запроса и падения стрима (таймер реконнекта запланирован).
|
|
113
|
+
await flush();
|
|
114
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
115
|
+
// Проходит базовая задержка → второй запрос.
|
|
116
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
117
|
+
expect(request).toHaveBeenCalledTimes(2);
|
|
118
|
+
// Второй стрим успешен → событие опубликовано.
|
|
119
|
+
await flush();
|
|
120
|
+
expect(received.length).toBe(1);
|
|
121
|
+
expect(received[0].sessionId).toBe('remote:ses-1');
|
|
122
|
+
bridge.stop();
|
|
123
|
+
});
|
|
124
|
+
it('backoff растёт экспоненциально до max', async () => {
|
|
125
|
+
vi.useFakeTimers();
|
|
126
|
+
const bus = new EventBus();
|
|
127
|
+
const request = vi.fn().mockResolvedValue(errorResponse());
|
|
128
|
+
const client = makeClient(request);
|
|
129
|
+
const bridge = new RemoteEventBridge({
|
|
130
|
+
client,
|
|
131
|
+
getToken: async () => 'tok',
|
|
132
|
+
eventBus: bus,
|
|
133
|
+
backendUrl: 'http://backend.test',
|
|
134
|
+
reconnectDelayMs: 1000,
|
|
135
|
+
maxReconnectDelayMs: 4000,
|
|
136
|
+
});
|
|
137
|
+
bridge.start();
|
|
138
|
+
await flush();
|
|
139
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
140
|
+
// Попытка 1: 1000 мс → запрос 2
|
|
141
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
142
|
+
await flush();
|
|
143
|
+
expect(request).toHaveBeenCalledTimes(2);
|
|
144
|
+
// Попытка 2: 2000 мс → запрос 3
|
|
145
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
146
|
+
await flush();
|
|
147
|
+
expect(request).toHaveBeenCalledTimes(3);
|
|
148
|
+
// Попытка 3: 4000 мс (cap) → запрос 4
|
|
149
|
+
await vi.advanceTimersByTimeAsync(4000);
|
|
150
|
+
await flush();
|
|
151
|
+
expect(request).toHaveBeenCalledTimes(4);
|
|
152
|
+
// Дальше задержка упирается в max 4000 — попытка 5 после ещё 4000 мс.
|
|
153
|
+
await vi.advanceTimersByTimeAsync(4000);
|
|
154
|
+
await flush();
|
|
155
|
+
expect(request).toHaveBeenCalledTimes(5);
|
|
156
|
+
bridge.stop();
|
|
157
|
+
});
|
|
158
|
+
it('stop() отменяет реконнект', async () => {
|
|
159
|
+
vi.useFakeTimers();
|
|
160
|
+
const bus = new EventBus();
|
|
161
|
+
const request = vi.fn().mockResolvedValue(errorResponse());
|
|
162
|
+
const client = makeClient(request);
|
|
163
|
+
const bridge = new RemoteEventBridge({
|
|
164
|
+
client,
|
|
165
|
+
getToken: async () => 'tok',
|
|
166
|
+
eventBus: bus,
|
|
167
|
+
backendUrl: 'http://backend.test',
|
|
168
|
+
reconnectDelayMs: 2000,
|
|
169
|
+
maxReconnectDelayMs: 10000,
|
|
170
|
+
});
|
|
171
|
+
bridge.start();
|
|
172
|
+
await flush();
|
|
173
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
174
|
+
bridge.stop();
|
|
175
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
176
|
+
expect(request).toHaveBeenCalledTimes(1); // реконнект отменён
|
|
177
|
+
});
|
|
178
|
+
it('shouldForward фильтрует события', async () => {
|
|
179
|
+
const bus = new EventBus();
|
|
180
|
+
const received = [];
|
|
181
|
+
bus.subscribeAll((e) => received.push(e));
|
|
182
|
+
const client = makeClient(vi.fn().mockResolvedValue(sseResponse([
|
|
183
|
+
'data: {"type":"stream.chunk","sessionId":"ses-1","data":{"stream_id":"st1","content":"x"},"timestamp":"t"}\n\n',
|
|
184
|
+
'data: {"type":"stream.done","sessionId":"ses-1","data":{"stream_id":"st1"},"timestamp":"t"}\n\n',
|
|
185
|
+
])));
|
|
186
|
+
const bridge = new RemoteEventBridge({
|
|
187
|
+
client,
|
|
188
|
+
getToken: async () => 'tok',
|
|
189
|
+
eventBus: bus,
|
|
190
|
+
backendUrl: 'http://backend.test',
|
|
191
|
+
shouldForward: (e) => e.type !== 'stream.chunk',
|
|
192
|
+
});
|
|
193
|
+
bridge.start();
|
|
194
|
+
await vi.waitFor(() => expect(received.length).toBe(1));
|
|
195
|
+
expect(received[0].type).toBe('stream.done');
|
|
196
|
+
bridge.stop();
|
|
197
|
+
});
|
|
198
|
+
it('onStatusChange вызывается при подключении и разрыве', async () => {
|
|
199
|
+
const bus = new EventBus();
|
|
200
|
+
const statuses = [];
|
|
201
|
+
const request = vi.fn()
|
|
202
|
+
.mockResolvedValueOnce(sseResponse([
|
|
203
|
+
'data: {"type":"stream.start","sessionId":"ses-1","data":{"stream_id":"st1"},"timestamp":"t"}\n\n',
|
|
204
|
+
]))
|
|
205
|
+
.mockResolvedValue(closedResponse()); // последующие стримы просто закрываются
|
|
206
|
+
const client = makeClient(request);
|
|
207
|
+
const bridge = new RemoteEventBridge({
|
|
208
|
+
client,
|
|
209
|
+
getToken: async () => 'tok',
|
|
210
|
+
eventBus: bus,
|
|
211
|
+
backendUrl: 'http://backend.test',
|
|
212
|
+
reconnectDelayMs: 60_000, // большая задержка, чтобы не мешал реконнект
|
|
213
|
+
});
|
|
214
|
+
bridge.onStatusChange = (c) => statuses.push(c);
|
|
215
|
+
bridge.start();
|
|
216
|
+
await vi.waitFor(() => expect(statuses.length).toBe(2), { timeout: 3000 });
|
|
217
|
+
expect(statuses).toEqual([true, false]);
|
|
218
|
+
bridge.stop();
|
|
219
|
+
});
|
|
220
|
+
it('события без sessionId игнорируются (не роняют стрим)', async () => {
|
|
221
|
+
const bus = new EventBus();
|
|
222
|
+
const received = [];
|
|
223
|
+
bus.subscribeAll((e) => received.push(e));
|
|
224
|
+
const client = makeClient(vi.fn().mockResolvedValue(sseResponse([
|
|
225
|
+
'data: {"type":"connected","data":{},"timestamp":"t"}\n\n',
|
|
226
|
+
'data: {"type":"stream.chunk","sessionId":"ses-1","data":{"stream_id":"st1","content":"x"},"timestamp":"t"}\n\n',
|
|
227
|
+
])));
|
|
228
|
+
const bridge = new RemoteEventBridge({
|
|
229
|
+
client,
|
|
230
|
+
getToken: async () => 'tok',
|
|
231
|
+
eventBus: bus,
|
|
232
|
+
backendUrl: 'http://backend.test',
|
|
233
|
+
});
|
|
234
|
+
bridge.start();
|
|
235
|
+
await vi.waitFor(() => expect(received.length).toBe(1));
|
|
236
|
+
expect(received[0].type).toBe('stream.chunk');
|
|
237
|
+
bridge.stop();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
//# sourceMappingURL=event-bridge.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-bridge.test.js","sourceRoot":"","sources":["../../src/remote/event-bridge.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { RemoteBackendClient } from './client.js';
|
|
2
|
+
export type { RemoteClientOptions, RemoteAuthStatus, RequestOptions, RemoteModelInfo } from './client.js';
|
|
3
|
+
export { RemoteEventBridge } from './event-bridge.js';
|
|
4
|
+
export type { RemoteEventBridgeOptions } from './event-bridge.js';
|
|
5
|
+
export { discover, startDeviceFlow, pollDeviceToken, refreshAccessToken, passwordGrant } from './device-flow.js';
|
|
6
|
+
export type { DeviceFlowDiscovery, DeviceFlowSession, DeviceTokenResult, RefreshTokenResult, PasswordGrantResult } from './device-flow.js';
|
|
7
|
+
export { loadCredentials, saveCredentials, clearCredentials, listCredentials } from './credentials.js';
|
|
8
|
+
export { RemoteSession } from './session.js';
|
|
9
|
+
export type { RemoteSessionOptions, RemoteSessionAuthStatus, DeviceCodeInfo } from './session.js';
|
|
10
|
+
export { RemoteConnection } from './connection.js';
|
|
11
|
+
export type { RemoteConnectionDeps } from './connection.js';
|
|
12
|
+
export { RemoteError } from './types.js';
|
|
13
|
+
export type { Origin, RemoteAuthType, RemoteCredentials, RemoteErrorCode, RemoteUser } from './types.js';
|
|
14
|
+
export { REMOTE_PREFIX, isRemoteRef, toLocalId, toRemoteRef, annotateOrigin, rewriteRemoteItem, toRemote, mergeItems, remoteErrorToHttp, } from './aggregate.js';
|
|
15
|
+
export type { MergeItemsOptions } from './aggregate.js';
|
|
16
|
+
export { syncProjectToRemote, forkProjectToLocal, walkDir, remoteWalk, buildSessionExport, resolveLocalWorkspace, slugify, isBinaryBuffer, EXCLUDED_DIRS, } from './transfer.js';
|
|
17
|
+
export type { SyncProjectToRemoteOptions, ForkProjectToLocalOptions, TransferStats } from './transfer.js';
|
|
18
|
+
export type { RemoteConfig } from '@itphx/synth-shared';
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/remote/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC1G,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjH,YAAY,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC3I,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACvG,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAClG,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,iBAAiB,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACzG,OAAO,EACL,aAAa,EACb,WAAW,EACX,SAAS,EACT,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,QAAQ,EACR,UAAU,EACV,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,OAAO,EACP,UAAU,EACV,kBAAkB,EAClB,qBAAqB,EACrB,OAAO,EACP,cAAc,EACd,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC1G,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Гибридный режим: remote-клиент, OIDC device flow, хранение credentials.
|
|
2
|
+
export { RemoteBackendClient } from './client.js';
|
|
3
|
+
export { RemoteEventBridge } from './event-bridge.js';
|
|
4
|
+
export { discover, startDeviceFlow, pollDeviceToken, refreshAccessToken, passwordGrant } from './device-flow.js';
|
|
5
|
+
export { loadCredentials, saveCredentials, clearCredentials, listCredentials } from './credentials.js';
|
|
6
|
+
export { RemoteSession } from './session.js';
|
|
7
|
+
export { RemoteConnection } from './connection.js';
|
|
8
|
+
export { RemoteError } from './types.js';
|
|
9
|
+
export { REMOTE_PREFIX, isRemoteRef, toLocalId, toRemoteRef, annotateOrigin, rewriteRemoteItem, toRemote, mergeItems, remoteErrorToHttp, } from './aggregate.js';
|
|
10
|
+
export { syncProjectToRemote, forkProjectToLocal, walkDir, remoteWalk, buildSessionExport, resolveLocalWorkspace, slugify, isBinaryBuffer, EXCLUDED_DIRS, } from './transfer.js';
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/remote/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAE1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACvG,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,OAAO,EACL,aAAa,EACb,WAAW,EACX,SAAS,EACT,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,QAAQ,EACR,UAAU,EACV,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,OAAO,EACP,UAAU,EACV,kBAAkB,EAClB,qBAAqB,EACrB,OAAO,EACP,cAAc,EACd,aAAa,GACd,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { RemoteBackendClient } from './client.js';
|
|
2
|
+
import type { RemoteAuthType, RemoteUser } from './types.js';
|
|
3
|
+
export interface RemoteSessionOptions {
|
|
4
|
+
backendUrl: string;
|
|
5
|
+
authType?: 'device' | 'api_key';
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
issuer?: string;
|
|
8
|
+
clientId?: string;
|
|
9
|
+
credentialsPath?: string;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Информация для показа пользователю во время device flow. */
|
|
13
|
+
export interface DeviceCodeInfo {
|
|
14
|
+
verificationUri: string;
|
|
15
|
+
userCode: string;
|
|
16
|
+
}
|
|
17
|
+
export interface RemoteSessionAuthStatus {
|
|
18
|
+
connected: boolean;
|
|
19
|
+
backendUrl: string;
|
|
20
|
+
user?: RemoteUser;
|
|
21
|
+
}
|
|
22
|
+
export declare class RemoteSession {
|
|
23
|
+
readonly backendUrl: string;
|
|
24
|
+
readonly authType: RemoteAuthType;
|
|
25
|
+
/** Серверный пользователь (из GET /auth/status), если известен. */
|
|
26
|
+
readonly user?: RemoteUser;
|
|
27
|
+
private _accessToken;
|
|
28
|
+
private _refreshToken?;
|
|
29
|
+
private _expiresAt?;
|
|
30
|
+
private readonly _issuer?;
|
|
31
|
+
private readonly _clientId?;
|
|
32
|
+
private readonly _client;
|
|
33
|
+
private readonly _credentialsPath?;
|
|
34
|
+
private constructor();
|
|
35
|
+
/**
|
|
36
|
+
* Обеспечить аутентифицированную сессию к backendUrl.
|
|
37
|
+
* 1. api_key: токен = apiKey (credentials не сохраняются — это не OAuth-токен, refresh неприменим).
|
|
38
|
+
* 2. Сохранённый accessToken ещё действителен → используем.
|
|
39
|
+
* 3. Просрочен + есть refreshToken + issuer/clientId → refreshAccessToken, обновить и сохранить.
|
|
40
|
+
* 4. Иначе → device flow: discover → startDeviceFlow → onDeviceCode → pollDeviceToken → сохранить.
|
|
41
|
+
*/
|
|
42
|
+
static ensureAuthenticated(opts: RemoteSessionOptions, onDeviceCode?: (info: DeviceCodeInfo) => Promise<void> | void): Promise<RemoteSession>;
|
|
43
|
+
/**
|
|
44
|
+
* Вход по логину/паролю (ROPC, grant_type=password) — «единая личность»:
|
|
45
|
+
* локальный core логинится как серверный пользователь (username + роли из
|
|
46
|
+
* Keycloak). Токены сохраняются в credentials (переживают рестарт,
|
|
47
|
+
* getToken() авто-обновляет по refresh token).
|
|
48
|
+
*
|
|
49
|
+
* После обмена токена запрашивается серверный пользователь
|
|
50
|
+
* (GET {backendUrl}/auth/status с Bearer access_token); если backend
|
|
51
|
+
* недоступен — credentials уже сохранены, user остаётся пустым.
|
|
52
|
+
*
|
|
53
|
+
* Ошибки: RemoteError('AUTH') — неверный логин/пароль;
|
|
54
|
+
* RemoteError('OFFLINE') — сервер авторизации недоступен.
|
|
55
|
+
*/
|
|
56
|
+
static loginWithPassword(opts: RemoteSessionOptions, username: string, password: string): Promise<RemoteSession>;
|
|
57
|
+
getClient(): RemoteBackendClient;
|
|
58
|
+
/**
|
|
59
|
+
* Токен для авторизации на удалённом backend (используется RemoteProvider
|
|
60
|
+
* как getToken — автообновление в рантайме).
|
|
61
|
+
* - api_key → apiKey;
|
|
62
|
+
* - accessToken действителен → как есть;
|
|
63
|
+
* - протух + refreshToken + issuer/clientId → refreshAccessToken, пересохранение credentials;
|
|
64
|
+
* - протух без refresh → null (RemoteProvider бросит ProviderError).
|
|
65
|
+
*/
|
|
66
|
+
getToken(): Promise<string | null>;
|
|
67
|
+
/** Статус подключения: OFFLINE → connected:false (перехват RemoteError), иначе пробрасываем. */
|
|
68
|
+
getAuthStatus(): Promise<RemoteSessionAuthStatus>;
|
|
69
|
+
/** Удалить сохранённые credentials для этого backendUrl. */
|
|
70
|
+
disconnect(): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/remote/session.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAIlD,OAAO,KAAK,EAAE,cAAc,EAAqB,UAAU,EAAE,MAAM,YAAY,CAAC;AAEhF,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,+DAA+D;AAC/D,MAAM,WAAW,cAAc;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AASD,qBAAa,aAAa;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,mEAAmE;IACnE,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC;IAE3B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,aAAa,CAAC,CAAS;IAC/B,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAC9C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAS;IAE3C,OAAO;IA8BP;;;;;;OAMG;WACU,mBAAmB,CAC9B,IAAI,EAAE,oBAAoB,EAC1B,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAC5D,OAAO,CAAC,aAAa,CAAC;IA8FzB;;;;;;;;;;;;OAYG;WACU,iBAAiB,CAC5B,IAAI,EAAE,oBAAoB,EAC1B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,aAAa,CAAC;IAmDzB,SAAS,IAAI,mBAAmB;IAIhC;;;;;;;OAOG;IACG,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA0CxC,gGAAgG;IAC1F,aAAa,IAAI,OAAO,CAAC,uBAAuB,CAAC;IAgBvD,4DAA4D;IACtD,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;CAGlC"}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// Фасад жизненного цикла подключения к удалённому backend (гибридный режим).
|
|
2
|
+
// ensureAuthenticated проходит путь: api_key → сохранённые credentials → refresh → device flow.
|
|
3
|
+
//
|
|
4
|
+
// Поток device flow (RFC 8628):
|
|
5
|
+
// discover → startDeviceFlow → onDeviceCode (печать URL+кода) → pollDeviceToken → сохранить credentials.
|
|
6
|
+
import { RemoteBackendClient } from './client.js';
|
|
7
|
+
import { loadCredentials, saveCredentials, clearCredentials } from './credentials.js';
|
|
8
|
+
import { startDeviceFlow, pollDeviceToken, refreshAccessToken, passwordGrant } from './device-flow.js';
|
|
9
|
+
import { RemoteError } from './types.js';
|
|
10
|
+
function isTokenExpired(expiresAt) {
|
|
11
|
+
if (!expiresAt)
|
|
12
|
+
return false;
|
|
13
|
+
const ts = Date.parse(expiresAt);
|
|
14
|
+
if (Number.isNaN(ts))
|
|
15
|
+
return false;
|
|
16
|
+
return ts <= Date.now();
|
|
17
|
+
}
|
|
18
|
+
export class RemoteSession {
|
|
19
|
+
backendUrl;
|
|
20
|
+
authType;
|
|
21
|
+
/** Серверный пользователь (из GET /auth/status), если известен. */
|
|
22
|
+
user;
|
|
23
|
+
_accessToken;
|
|
24
|
+
_refreshToken;
|
|
25
|
+
_expiresAt;
|
|
26
|
+
_issuer;
|
|
27
|
+
_clientId;
|
|
28
|
+
_client;
|
|
29
|
+
_credentialsPath;
|
|
30
|
+
constructor(backendUrl, authType, accessToken, credentialsPath, timeoutMs, refreshToken, expiresAt, issuer, clientId, user) {
|
|
31
|
+
this.backendUrl = backendUrl;
|
|
32
|
+
this.authType = authType;
|
|
33
|
+
this._accessToken = accessToken;
|
|
34
|
+
this._refreshToken = refreshToken;
|
|
35
|
+
this._expiresAt = expiresAt;
|
|
36
|
+
this._issuer = issuer;
|
|
37
|
+
this._clientId = clientId;
|
|
38
|
+
this._credentialsPath = credentialsPath;
|
|
39
|
+
this.user = user;
|
|
40
|
+
this._client = new RemoteBackendClient({
|
|
41
|
+
backendUrl,
|
|
42
|
+
// Клиент тоже ходит через session.getToken(): протухший accessToken
|
|
43
|
+
// автоматически обновляется в рантайме (а не уходит на backend как есть).
|
|
44
|
+
getToken: () => this.getToken(),
|
|
45
|
+
timeoutMs,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Обеспечить аутентифицированную сессию к backendUrl.
|
|
50
|
+
* 1. api_key: токен = apiKey (credentials не сохраняются — это не OAuth-токен, refresh неприменим).
|
|
51
|
+
* 2. Сохранённый accessToken ещё действителен → используем.
|
|
52
|
+
* 3. Просрочен + есть refreshToken + issuer/clientId → refreshAccessToken, обновить и сохранить.
|
|
53
|
+
* 4. Иначе → device flow: discover → startDeviceFlow → onDeviceCode → pollDeviceToken → сохранить.
|
|
54
|
+
*/
|
|
55
|
+
static async ensureAuthenticated(opts, onDeviceCode) {
|
|
56
|
+
const backendUrl = opts.backendUrl.replace(/\/+$/, '');
|
|
57
|
+
const authType = opts.authType ?? 'device';
|
|
58
|
+
const credentialsPath = opts.credentialsPath;
|
|
59
|
+
if (authType === 'api_key') {
|
|
60
|
+
if (!opts.apiKey) {
|
|
61
|
+
throw new RemoteError('Для remote.auth.type=api_key укажите API-ключ (флаг --token или remote.auth.apiKey в config.yaml)', 'AUTH');
|
|
62
|
+
}
|
|
63
|
+
return new RemoteSession(backendUrl, authType, opts.apiKey, credentialsPath, opts.timeoutMs);
|
|
64
|
+
}
|
|
65
|
+
const existing = await loadCredentials(backendUrl, credentialsPath);
|
|
66
|
+
// Сохранённый accessToken действителен — используем как есть.
|
|
67
|
+
if (existing?.accessToken && !isTokenExpired(existing.expiresAt)) {
|
|
68
|
+
return new RemoteSession(backendUrl, authType, existing.accessToken, credentialsPath, opts.timeoutMs, existing.refreshToken, existing.expiresAt, opts.issuer, opts.clientId, existing.user);
|
|
69
|
+
}
|
|
70
|
+
// Просрочен, но есть refreshToken — пробуем обновить.
|
|
71
|
+
if (existing?.refreshToken) {
|
|
72
|
+
if (!opts.issuer || !opts.clientId) {
|
|
73
|
+
throw new RemoteError('Не заданы remote.auth.issuer/clientId — укажите их в config.yaml или флагами --oidc-issuer/--oidc-client-id', 'AUTH');
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const refreshed = await refreshAccessToken(opts.issuer, opts.clientId, existing.refreshToken);
|
|
77
|
+
const updated = {
|
|
78
|
+
backendUrl,
|
|
79
|
+
accessToken: refreshed.accessToken,
|
|
80
|
+
refreshToken: refreshed.refreshToken ?? existing.refreshToken,
|
|
81
|
+
expiresAt: refreshed.expiresIn
|
|
82
|
+
? new Date(Date.now() + refreshed.expiresIn * 1000).toISOString()
|
|
83
|
+
: undefined,
|
|
84
|
+
user: existing.user,
|
|
85
|
+
};
|
|
86
|
+
await saveCredentials(updated, credentialsPath);
|
|
87
|
+
return new RemoteSession(backendUrl, authType, updated.accessToken, credentialsPath, opts.timeoutMs, updated.refreshToken, updated.expiresAt, opts.issuer, opts.clientId, updated.user);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
if (err instanceof RemoteError && err.code === 'AUTH') {
|
|
91
|
+
// refresh token недействителен (invalid_grant и т.п.) — чистим и проходим device flow заново.
|
|
92
|
+
await clearCredentials(backendUrl, credentialsPath).catch(() => { });
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Ничего нет (или refresh не удался) → device flow.
|
|
100
|
+
if (!opts.issuer || !opts.clientId) {
|
|
101
|
+
throw new RemoteError('Не заданы remote.auth.issuer/clientId — укажите их в config.yaml или флагами --oidc-issuer/--oidc-client-id', 'AUTH');
|
|
102
|
+
}
|
|
103
|
+
const deviceSession = await startDeviceFlow(opts.issuer, opts.clientId);
|
|
104
|
+
if (onDeviceCode) {
|
|
105
|
+
await onDeviceCode({
|
|
106
|
+
verificationUri: deviceSession.verificationUri,
|
|
107
|
+
userCode: deviceSession.userCode,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const token = await pollDeviceToken(deviceSession);
|
|
111
|
+
const creds = {
|
|
112
|
+
backendUrl,
|
|
113
|
+
accessToken: token.accessToken,
|
|
114
|
+
refreshToken: token.refreshToken,
|
|
115
|
+
expiresAt: token.expiresIn
|
|
116
|
+
? new Date(Date.now() + token.expiresIn * 1000).toISOString()
|
|
117
|
+
: undefined,
|
|
118
|
+
// id_token не парсим на этом этапе — user оставляем пустым.
|
|
119
|
+
};
|
|
120
|
+
await saveCredentials(creds, credentialsPath);
|
|
121
|
+
return new RemoteSession(backendUrl, authType, creds.accessToken, credentialsPath, opts.timeoutMs, creds.refreshToken, creds.expiresAt, opts.issuer, opts.clientId);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Вход по логину/паролю (ROPC, grant_type=password) — «единая личность»:
|
|
125
|
+
* локальный core логинится как серверный пользователь (username + роли из
|
|
126
|
+
* Keycloak). Токены сохраняются в credentials (переживают рестарт,
|
|
127
|
+
* getToken() авто-обновляет по refresh token).
|
|
128
|
+
*
|
|
129
|
+
* После обмена токена запрашивается серверный пользователь
|
|
130
|
+
* (GET {backendUrl}/auth/status с Bearer access_token); если backend
|
|
131
|
+
* недоступен — credentials уже сохранены, user остаётся пустым.
|
|
132
|
+
*
|
|
133
|
+
* Ошибки: RemoteError('AUTH') — неверный логин/пароль;
|
|
134
|
+
* RemoteError('OFFLINE') — сервер авторизации недоступен.
|
|
135
|
+
*/
|
|
136
|
+
static async loginWithPassword(opts, username, password) {
|
|
137
|
+
const backendUrl = opts.backendUrl.replace(/\/+$/, '');
|
|
138
|
+
const authType = opts.authType ?? 'device';
|
|
139
|
+
if (!opts.issuer || !opts.clientId) {
|
|
140
|
+
throw new RemoteError('Не заданы remote.auth.issuer/clientId — укажите их в config.yaml или флагами --oidc-issuer/--oidc-client-id', 'AUTH');
|
|
141
|
+
}
|
|
142
|
+
if (!username || !password) {
|
|
143
|
+
throw new RemoteError('Укажите логин и пароль', 'AUTH');
|
|
144
|
+
}
|
|
145
|
+
const granted = await passwordGrant(opts.issuer, opts.clientId, username, password);
|
|
146
|
+
const expiresAt = granted.expiresIn
|
|
147
|
+
? new Date(Date.now() + granted.expiresIn * 1000).toISOString()
|
|
148
|
+
: undefined;
|
|
149
|
+
const creds = {
|
|
150
|
+
backendUrl,
|
|
151
|
+
accessToken: granted.accessToken,
|
|
152
|
+
refreshToken: granted.refreshToken,
|
|
153
|
+
expiresAt,
|
|
154
|
+
};
|
|
155
|
+
await saveCredentials(creds, opts.credentialsPath);
|
|
156
|
+
// Пытаемся получить серверного пользователя. Если backend недоступен —
|
|
157
|
+
// сессия уже создана, user остаётся пустым (credentials без user сохранены).
|
|
158
|
+
let user;
|
|
159
|
+
try {
|
|
160
|
+
const statusClient = new RemoteBackendClient({
|
|
161
|
+
backendUrl,
|
|
162
|
+
getToken: async () => granted.accessToken,
|
|
163
|
+
timeoutMs: opts.timeoutMs,
|
|
164
|
+
});
|
|
165
|
+
const status = await statusClient.getAuthStatus();
|
|
166
|
+
if (status.user) {
|
|
167
|
+
user = { id: status.user.id, username: status.user.username, roles: status.user.roles };
|
|
168
|
+
await saveCredentials({ ...creds, user }, opts.credentialsPath);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// статус недоступен — не роняем логин
|
|
173
|
+
}
|
|
174
|
+
return new RemoteSession(backendUrl, authType, creds.accessToken, opts.credentialsPath, opts.timeoutMs, creds.refreshToken, creds.expiresAt, opts.issuer, opts.clientId, user);
|
|
175
|
+
}
|
|
176
|
+
getClient() {
|
|
177
|
+
return this._client;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Токен для авторизации на удалённом backend (используется RemoteProvider
|
|
181
|
+
* как getToken — автообновление в рантайме).
|
|
182
|
+
* - api_key → apiKey;
|
|
183
|
+
* - accessToken действителен → как есть;
|
|
184
|
+
* - протух + refreshToken + issuer/clientId → refreshAccessToken, пересохранение credentials;
|
|
185
|
+
* - протух без refresh → null (RemoteProvider бросит ProviderError).
|
|
186
|
+
*/
|
|
187
|
+
async getToken() {
|
|
188
|
+
if (this.authType === 'api_key') {
|
|
189
|
+
return this._accessToken;
|
|
190
|
+
}
|
|
191
|
+
if (!isTokenExpired(this._expiresAt)) {
|
|
192
|
+
return this._accessToken;
|
|
193
|
+
}
|
|
194
|
+
if (this._refreshToken && this._issuer && this._clientId) {
|
|
195
|
+
try {
|
|
196
|
+
const refreshed = await refreshAccessToken(this._issuer, this._clientId, this._refreshToken);
|
|
197
|
+
this._accessToken = refreshed.accessToken;
|
|
198
|
+
if (refreshed.refreshToken)
|
|
199
|
+
this._refreshToken = refreshed.refreshToken;
|
|
200
|
+
this._expiresAt = refreshed.expiresIn
|
|
201
|
+
? new Date(Date.now() + refreshed.expiresIn * 1000).toISOString()
|
|
202
|
+
: undefined;
|
|
203
|
+
// Персист: обновляем сохранённые credentials, если есть (иначе после
|
|
204
|
+
// рестарта сессия возьмёт протухший токен из файла).
|
|
205
|
+
const existing = await loadCredentials(this.backendUrl, this._credentialsPath);
|
|
206
|
+
if (existing) {
|
|
207
|
+
await saveCredentials({
|
|
208
|
+
...existing,
|
|
209
|
+
accessToken: refreshed.accessToken,
|
|
210
|
+
refreshToken: refreshed.refreshToken ?? existing.refreshToken,
|
|
211
|
+
expiresAt: this._expiresAt,
|
|
212
|
+
}, this._credentialsPath);
|
|
213
|
+
}
|
|
214
|
+
return refreshed.accessToken;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Ошибка refresh (RemoteError('AUTH') — невалидный/истёкший refresh token,
|
|
218
|
+
// либо сеть недоступна) НЕ роняем поток: возвращаем null. Запрос уйдёт без
|
|
219
|
+
// токена и получит 401/ошибку сети (UNAUTHORIZED/OFFLINE), а RemoteProvider
|
|
220
|
+
// бросит ProviderError «токен истёк» — корректная offline-деградация.
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
/** Статус подключения: OFFLINE → connected:false (перехват RemoteError), иначе пробрасываем. */
|
|
227
|
+
async getAuthStatus() {
|
|
228
|
+
try {
|
|
229
|
+
const status = await this._client.getAuthStatus();
|
|
230
|
+
return {
|
|
231
|
+
connected: status.authenticated,
|
|
232
|
+
backendUrl: this.backendUrl,
|
|
233
|
+
user: status.user ?? undefined,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
if (err instanceof RemoteError && err.code === 'OFFLINE') {
|
|
238
|
+
return { connected: false, backendUrl: this.backendUrl };
|
|
239
|
+
}
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** Удалить сохранённые credentials для этого backendUrl. */
|
|
244
|
+
async disconnect() {
|
|
245
|
+
await clearCredentials(this.backendUrl, this._credentialsPath);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
//# sourceMappingURL=session.js.map
|