@ouispec/agent-realtime 0.1.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/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/approvals/client-event.d.ts +4 -0
- package/dist/approvals/client-event.d.ts.map +1 -0
- package/dist/approvals/client-event.js +25 -0
- package/dist/approvals/client-event.js.map +1 -0
- package/dist/approvals/routes.d.ts +19 -0
- package/dist/approvals/routes.d.ts.map +1 -0
- package/dist/approvals/routes.js +100 -0
- package/dist/approvals/routes.js.map +1 -0
- package/dist/approvals/schemas.d.ts +7 -0
- package/dist/approvals/schemas.d.ts.map +1 -0
- package/dist/approvals/schemas.js +45 -0
- package/dist/approvals/schemas.js.map +1 -0
- package/dist/approvals/store.d.ts +33 -0
- package/dist/approvals/store.d.ts.map +1 -0
- package/dist/approvals/store.js +206 -0
- package/dist/approvals/store.js.map +1 -0
- package/dist/approvals/token.d.ts +19 -0
- package/dist/approvals/token.d.ts.map +1 -0
- package/dist/approvals/token.js +73 -0
- package/dist/approvals/token.js.map +1 -0
- package/dist/client-events/builtin.d.ts +30 -0
- package/dist/client-events/builtin.d.ts.map +1 -0
- package/dist/client-events/builtin.js +161 -0
- package/dist/client-events/builtin.js.map +1 -0
- package/dist/client-events/chain.d.ts +80 -0
- package/dist/client-events/chain.d.ts.map +1 -0
- package/dist/client-events/chain.js +92 -0
- package/dist/client-events/chain.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +85 -0
- package/dist/config.js.map +1 -0
- package/dist/events/declared.d.ts +18 -0
- package/dist/events/declared.d.ts.map +1 -0
- package/dist/events/declared.js +30 -0
- package/dist/events/declared.js.map +1 -0
- package/dist/events/settlements.d.ts +28 -0
- package/dist/events/settlements.d.ts.map +1 -0
- package/dist/events/settlements.js +29 -0
- package/dist/events/settlements.js.map +1 -0
- package/dist/http/routes.d.ts +91 -0
- package/dist/http/routes.d.ts.map +1 -0
- package/dist/http/routes.js +290 -0
- package/dist/http/routes.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +14 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +22 -0
- package/dist/logger.js.map +1 -0
- package/dist/oui/results.d.ts +46 -0
- package/dist/oui/results.d.ts.map +1 -0
- package/dist/oui/results.js +56 -0
- package/dist/oui/results.js.map +1 -0
- package/dist/redis-waits.d.ts +32 -0
- package/dist/redis-waits.d.ts.map +1 -0
- package/dist/redis-waits.js +62 -0
- package/dist/redis-waits.js.map +1 -0
- package/dist/rooms/join.d.ts +13 -0
- package/dist/rooms/join.d.ts.map +1 -0
- package/dist/rooms/join.js +27 -0
- package/dist/rooms/join.js.map +1 -0
- package/dist/rooms/room-token.d.ts +19 -0
- package/dist/rooms/room-token.d.ts.map +1 -0
- package/dist/rooms/room-token.js +55 -0
- package/dist/rooms/room-token.js.map +1 -0
- package/dist/server.d.ts +33 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +168 -0
- package/dist/server.js.map +1 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +87 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types.d.ts +135 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OUI action results, held for the agent worker that asked (ADR-0209 D2).
|
|
3
|
+
*
|
|
4
|
+
* The worker sends a UI action to the turn's room and then waits here for the
|
|
5
|
+
* client's answer. The answer arrives on whichever realtime instance holds the
|
|
6
|
+
* client's socket, and the worker's wait may be served by another, so both
|
|
7
|
+
* meet in Redis:
|
|
8
|
+
*
|
|
9
|
+
* oui:result:{requestId} → JSON { userId, result }, TTL 5 min, first answer wins
|
|
10
|
+
* oui:result:{requestId}:final → JSON { userId, result }, TTL 35 min, the final answer
|
|
11
|
+
* channel oui:results → the key's id (`{requestId}` or `{requestId}:final`),
|
|
12
|
+
* published when an answer is stored
|
|
13
|
+
*
|
|
14
|
+
* An async action (a GPU job, an export) is answered twice: acknowledged
|
|
15
|
+
* (`interim: true`, "started"), then finished (`interim: false`) once its
|
|
16
|
+
* output exists or it has failed. The first answer is what a worker's first
|
|
17
|
+
* wait gets. The final answer is kept apart, for as long as the longest job
|
|
18
|
+
* may run, so a worker can wait on to it and tell the person the work is done
|
|
19
|
+
* only once it is. A synchronous action's one answer is both.
|
|
20
|
+
*
|
|
21
|
+
* An answer is kept under the user of the socket that sent it, and handed only
|
|
22
|
+
* to a wait for that same user.
|
|
23
|
+
*/
|
|
24
|
+
import type { OUIActionResult } from 'oui-spec/spec';
|
|
25
|
+
import type { RealtimeLogger } from '../logger.js';
|
|
26
|
+
import { type ResultsRedis, type ResultsSubscriber } from '../redis-waits.js';
|
|
27
|
+
export type { ResultsRedis, ResultsSubscriber };
|
|
28
|
+
export interface OUIResultStore {
|
|
29
|
+
/** Keep a client's answer. Returns false when an answer for the request was already kept. */
|
|
30
|
+
store(userId: string, result: OUIActionResult): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* The answer for `requestId` given by `userId`, waiting up to `waitMs` for it
|
|
33
|
+
* to arrive: the first answer, or with `final` the final one (never an
|
|
34
|
+
* acknowledgment).
|
|
35
|
+
*/
|
|
36
|
+
await(requestId: string, userId: string, waitMs: number, options?: {
|
|
37
|
+
final?: boolean;
|
|
38
|
+
}): Promise<OUIActionResult | null>;
|
|
39
|
+
/** Waits in progress on this instance. */
|
|
40
|
+
pendingCount(): number;
|
|
41
|
+
}
|
|
42
|
+
export declare const RESULT_TTL_SEC: number;
|
|
43
|
+
/** As long as the longest job an action follows may run (an export, a recipient batch: 30 min), with margin. */
|
|
44
|
+
export declare const FINAL_RESULT_TTL_SEC: number;
|
|
45
|
+
export declare function createOUIResultStore(redis: ResultsRedis, subscriber: ResultsSubscriber, logger: RealtimeLogger): OUIResultStore;
|
|
46
|
+
//# sourceMappingURL=results.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../../src/oui/results.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAiB,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE7F,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,CAAC;AAEhD,MAAM,WAAW,cAAc;IAC7B,6FAA6F;IAC7F,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE;;;;OAIG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;IACzH,0CAA0C;IAC1C,YAAY,IAAI,MAAM,CAAC;CACxB;AAGD,eAAO,MAAM,cAAc,QAAS,CAAC;AACrC,gHAAgH;AAChH,eAAO,MAAM,oBAAoB,QAAU,CAAC;AAW5C,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,YAAY,EACnB,UAAU,EAAE,iBAAiB,EAC7B,MAAM,EAAE,cAAc,GACrB,cAAc,CAmDhB"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createWaitHub } from '../redis-waits.js';
|
|
2
|
+
const CHANNEL = 'oui:results';
|
|
3
|
+
export const RESULT_TTL_SEC = 5 * 60;
|
|
4
|
+
/** As long as the longest job an action follows may run (an export, a recipient batch: 30 min), with margin. */
|
|
5
|
+
export const FINAL_RESULT_TTL_SEC = 35 * 60;
|
|
6
|
+
/** What a wait is for, as the channel and the waiters name it: the first answer, or the final one. */
|
|
7
|
+
const waitId = (requestId, final) => (final ? `${requestId}:final` : requestId);
|
|
8
|
+
const resultKey = (id) => `oui:result:${id}`;
|
|
9
|
+
export function createOUIResultStore(redis, subscriber, logger) {
|
|
10
|
+
const hub = createWaitHub(subscriber, CHANNEL, logger, 'OUI results');
|
|
11
|
+
async function read(requestId, userId, id) {
|
|
12
|
+
const raw = await redis.get(resultKey(id));
|
|
13
|
+
if (!raw)
|
|
14
|
+
return null;
|
|
15
|
+
const stored = JSON.parse(raw);
|
|
16
|
+
if (stored.userId !== userId) {
|
|
17
|
+
// Another user's answer for this id is not an answer to this user.
|
|
18
|
+
logger.warn({ requestId, askedFor: userId, answeredBy: stored.userId }, 'OUI result belongs to another user');
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return stored.result;
|
|
22
|
+
}
|
|
23
|
+
/** Keep a final answer, the first one only, and wake its waits. */
|
|
24
|
+
async function storeFinal(stored) {
|
|
25
|
+
const id = waitId(stored.result.requestId, true);
|
|
26
|
+
const written = await redis.set(resultKey(id), JSON.stringify(stored), 'EX', FINAL_RESULT_TTL_SEC, 'NX');
|
|
27
|
+
if (written !== 'OK') {
|
|
28
|
+
logger.debug({ requestId: stored.result.requestId }, 'OUI final result already held; later answer not kept');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
await redis.publish(CHANNEL, id);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
async store(userId, result) {
|
|
35
|
+
const stored = { userId, result };
|
|
36
|
+
if (result.interim !== true)
|
|
37
|
+
await storeFinal(stored);
|
|
38
|
+
const written = await redis.set(resultKey(result.requestId), JSON.stringify(stored), 'EX', RESULT_TTL_SEC, 'NX');
|
|
39
|
+
if (written !== 'OK') {
|
|
40
|
+
// An async action is answered twice (acknowledged, then finished). The
|
|
41
|
+
// first wait gets the first; the finished answer is kept as the final
|
|
42
|
+
// one (above), for a worker that waits on to it.
|
|
43
|
+
logger.debug({ requestId: result.requestId, interim: result.interim }, 'OUI result already held; later answer not kept');
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
await redis.publish(CHANNEL, result.requestId);
|
|
47
|
+
return true;
|
|
48
|
+
},
|
|
49
|
+
async await(requestId, userId, waitMs, options = {}) {
|
|
50
|
+
const id = waitId(requestId, options.final === true);
|
|
51
|
+
return hub.wait(id, waitMs, () => read(requestId, userId, id), (err) => logger.error({ err, requestId }, 'OUI result read failed'));
|
|
52
|
+
},
|
|
53
|
+
pendingCount: () => hub.pendingCount(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=results.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"results.js","sourceRoot":"","sources":["../../src/oui/results.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waiting on something another instance may store: the mechanism the UI
|
|
3
|
+
* action result store and the settlement store share.
|
|
4
|
+
*
|
|
5
|
+
* A value is stored in Redis and its id published on a channel. A wait reads
|
|
6
|
+
* it at once, then again each time its id is published, and once more at its
|
|
7
|
+
* deadline in case a notification was missed. Waits on one instance share one
|
|
8
|
+
* subscription.
|
|
9
|
+
*/
|
|
10
|
+
import type { RealtimeLogger } from './logger.js';
|
|
11
|
+
/** The Redis commands the stores use. ioredis satisfies it. */
|
|
12
|
+
export interface ResultsRedis {
|
|
13
|
+
set(key: string, value: string, ex: 'EX', seconds: number, nx: 'NX'): Promise<'OK' | null>;
|
|
14
|
+
get(key: string): Promise<string | null>;
|
|
15
|
+
publish(channel: string, message: string): Promise<number>;
|
|
16
|
+
}
|
|
17
|
+
/** A connection in subscriber mode. ioredis satisfies it. */
|
|
18
|
+
export interface ResultsSubscriber {
|
|
19
|
+
subscribe(channel: string): Promise<unknown>;
|
|
20
|
+
on(event: 'message', listener: (channel: string, message: string) => void): unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface WaitHub {
|
|
23
|
+
/**
|
|
24
|
+
* `read()`'s value once it has one, reading at once, on every publish of
|
|
25
|
+
* `id`, and at the deadline; null when it has none by `waitMs`.
|
|
26
|
+
*/
|
|
27
|
+
wait<T>(id: string, waitMs: number, read: () => Promise<T | null>, onReadError: (err: unknown) => void): Promise<T | null>;
|
|
28
|
+
/** Waits in progress on this instance. */
|
|
29
|
+
pendingCount(): number;
|
|
30
|
+
}
|
|
31
|
+
export declare function createWaitHub(subscriber: ResultsSubscriber, channel: string, logger: RealtimeLogger, label: string): WaitHub;
|
|
32
|
+
//# sourceMappingURL=redis-waits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-waits.d.ts","sourceRoot":"","sources":["../src/redis-waits.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,+DAA+D;AAC/D,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC3F,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC5D;AAED,6DAA6D;AAC7D,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC;CACrF;AAED,MAAM,WAAW,OAAO;IACtB;;;OAGG;IACH,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3H,0CAA0C;IAC1C,YAAY,IAAI,MAAM,CAAC;CACxB;AAED,wBAAgB,aAAa,CAAC,UAAU,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAyD5H"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export function createWaitHub(subscriber, channel, logger, label) {
|
|
2
|
+
const waiters = new Map();
|
|
3
|
+
subscriber.on('message', (ch, id) => {
|
|
4
|
+
if (ch !== channel)
|
|
5
|
+
return;
|
|
6
|
+
const pending = waiters.get(id);
|
|
7
|
+
if (!pending)
|
|
8
|
+
return;
|
|
9
|
+
for (const wake of [...pending])
|
|
10
|
+
wake();
|
|
11
|
+
});
|
|
12
|
+
subscriber.subscribe(channel).catch((err) => {
|
|
13
|
+
logger.error({ err }, `${label}: subscribe failed — waits will only see what was stored before they began`);
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
async wait(id, waitMs, read, onReadError) {
|
|
17
|
+
const now = await read();
|
|
18
|
+
if (now || waitMs <= 0)
|
|
19
|
+
return now;
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
let done = false;
|
|
22
|
+
const finish = (value) => {
|
|
23
|
+
if (done)
|
|
24
|
+
return;
|
|
25
|
+
done = true;
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
const set = waiters.get(id);
|
|
28
|
+
set?.delete(wake);
|
|
29
|
+
if (set && set.size === 0)
|
|
30
|
+
waiters.delete(id);
|
|
31
|
+
resolve(value);
|
|
32
|
+
};
|
|
33
|
+
const wake = () => {
|
|
34
|
+
read().then((r) => {
|
|
35
|
+
if (r)
|
|
36
|
+
finish(r);
|
|
37
|
+
}, onReadError);
|
|
38
|
+
};
|
|
39
|
+
const timer = setTimeout(() => {
|
|
40
|
+
// One last look before giving up, in case the notification was missed.
|
|
41
|
+
read().then(finish, () => finish(null));
|
|
42
|
+
}, waitMs);
|
|
43
|
+
let set = waiters.get(id);
|
|
44
|
+
if (!set) {
|
|
45
|
+
set = new Set();
|
|
46
|
+
waiters.set(id, set);
|
|
47
|
+
}
|
|
48
|
+
set.add(wake);
|
|
49
|
+
// Close the gap between the first read and registering the waiter: a
|
|
50
|
+
// value stored in between was published before anyone was listening.
|
|
51
|
+
wake();
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
pendingCount() {
|
|
55
|
+
let n = 0;
|
|
56
|
+
for (const set of waiters.values())
|
|
57
|
+
n += set.size;
|
|
58
|
+
return n;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=redis-waits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-waits.js","sourceRoot":"","sources":["../src/redis-waits.ts"],"names":[],"mappings":"AAkCA,MAAM,UAAU,aAAa,CAAC,UAA6B,EAAE,OAAe,EAAE,MAAsB,EAAE,KAAa;IACjH,MAAM,OAAO,GAAG,IAAI,GAAG,EAA2B,CAAC;IAEnD,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;QAClC,IAAI,EAAE,KAAK,OAAO;YAAE,OAAO;QAC3B,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChC,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,OAAO,CAAC;YAAE,IAAI,EAAE,CAAC;IAC1C,CAAC,CAAC,CAAC;IACH,UAAU,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACnD,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,GAAG,KAAK,4EAA4E,CAAC,CAAC;IAC9G,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,KAAK,CAAC,IAAI,CAAI,EAAU,EAAE,MAAc,EAAE,IAA6B,EAAE,WAAmC;YAC1G,MAAM,GAAG,GAAG,MAAM,IAAI,EAAE,CAAC;YACzB,IAAI,GAAG,IAAI,MAAM,IAAI,CAAC;gBAAE,OAAO,GAAG,CAAC;YAEnC,OAAO,IAAI,OAAO,CAAW,CAAC,OAAO,EAAE,EAAE;gBACvC,IAAI,IAAI,GAAG,KAAK,CAAC;gBACjB,MAAM,MAAM,GAAG,CAAC,KAAe,EAAE,EAAE;oBACjC,IAAI,IAAI;wBAAE,OAAO;oBACjB,IAAI,GAAG,IAAI,CAAC;oBACZ,YAAY,CAAC,KAAK,CAAC,CAAC;oBACpB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;oBAC5B,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;oBAClB,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC;wBAAE,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;oBAC9C,OAAO,CAAC,KAAK,CAAC,CAAC;gBACjB,CAAC,CAAC;gBACF,MAAM,IAAI,GAAG,GAAG,EAAE;oBAChB,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;wBAChB,IAAI,CAAC;4BAAE,MAAM,CAAC,CAAC,CAAC,CAAC;oBACnB,CAAC,EAAE,WAAW,CAAC,CAAC;gBAClB,CAAC,CAAC;gBACF,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;oBAC5B,uEAAuE;oBACvE,IAAI,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;gBAC1C,CAAC,EAAE,MAAM,CAAC,CAAC;gBAEX,IAAI,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBAC1B,IAAI,CAAC,GAAG,EAAE,CAAC;oBACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;oBAChB,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;gBACvB,CAAC;gBACD,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACd,qEAAqE;gBACrE,qEAAqE;gBACrE,IAAI,EAAE,CAAC;YACT,CAAC,CAAC,CAAC;QACL,CAAC;QAED,YAAY;YACV,IAAI,CAAC,GAAG,CAAC,CAAC;YACV,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,MAAM,EAAE;gBAAE,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC;YAClD,OAAO,CAAC,CAAC;QACX,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AuthResult, RoomPolicy } from '../types.js';
|
|
2
|
+
import type { RoomTokenSigner } from './room-token.js';
|
|
3
|
+
/**
|
|
4
|
+
* Why `user` may not join `room`, or null when it may.
|
|
5
|
+
*
|
|
6
|
+
* - An invalid room is refused.
|
|
7
|
+
* - A room that `requiresToken` needs a token binding this user to this exact
|
|
8
|
+
* room. Tokens are minted only after the product's own authorization for
|
|
9
|
+
* the resource (see `RoomPolicy`), so the token is the whole check.
|
|
10
|
+
* - Any other room is the policy's `canJoin`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function joinRefusal(policy: RoomPolicy, tokens: RoomTokenSigner, room: string, user: AuthResult, roomToken?: string): string | null;
|
|
13
|
+
//# sourceMappingURL=join.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"join.d.ts","sourceRoot":"","sources":["../../src/rooms/join.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,UAAU,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAWf"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why `user` may not join `room`, or null when it may.
|
|
3
|
+
*
|
|
4
|
+
* - An invalid room is refused.
|
|
5
|
+
* - A room that `requiresToken` needs a token binding this user to this exact
|
|
6
|
+
* room. Tokens are minted only after the product's own authorization for
|
|
7
|
+
* the resource (see `RoomPolicy`), so the token is the whole check.
|
|
8
|
+
* - Any other room is the policy's `canJoin`.
|
|
9
|
+
*/
|
|
10
|
+
export function joinRefusal(policy, tokens, room, user, roomToken) {
|
|
11
|
+
if (!policy.isValidRoom(room))
|
|
12
|
+
return 'invalid room';
|
|
13
|
+
if (policy.requiresToken(room)) {
|
|
14
|
+
if (!roomToken)
|
|
15
|
+
return 'room token required';
|
|
16
|
+
const payload = tokens.verify(roomToken);
|
|
17
|
+
if (!payload)
|
|
18
|
+
return 'room token invalid or expired';
|
|
19
|
+
if (payload.userId !== user.userId)
|
|
20
|
+
return 'room token belongs to another user';
|
|
21
|
+
if (payload.room !== room)
|
|
22
|
+
return 'room token is for another room';
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return policy.canJoin(room, user) ? null : 'not allowed';
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=join.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"join.js","sourceRoot":"","sources":["../../src/rooms/join.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CACzB,MAAkB,EAClB,MAAuB,EACvB,IAAY,EACZ,IAAgB,EAChB,SAAkB;IAElB,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC;QAAE,OAAO,cAAc,CAAC;IACrD,IAAI,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,IAAI,CAAC,SAAS;YAAE,OAAO,qBAAqB,CAAC;QAC7C,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACzC,IAAI,CAAC,OAAO;YAAE,OAAO,+BAA+B,CAAC;QACrD,IAAI,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM;YAAE,OAAO,oCAAoC,CAAC;QAChF,IAAI,OAAO,CAAC,IAAI,KAAK,IAAI;YAAE,OAAO,gCAAgC,CAAC;QACnE,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface RoomTokenPayload {
|
|
2
|
+
userId: string;
|
|
3
|
+
room: string;
|
|
4
|
+
/** Expiry, epoch milliseconds. */
|
|
5
|
+
exp: number;
|
|
6
|
+
}
|
|
7
|
+
export interface RoomTokenSigner {
|
|
8
|
+
sign(userId: string, room: string): string;
|
|
9
|
+
/** The payload of a valid, unexpired token, or null for anything else. */
|
|
10
|
+
verify(token: string): RoomTokenPayload | null;
|
|
11
|
+
readonly ttlMs: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const DEFAULT_ROOM_TOKEN_TTL_MS: number;
|
|
14
|
+
export declare const MIN_ROOM_TOKEN_SECRET_LENGTH = 32;
|
|
15
|
+
export declare function createRoomTokenSigner(options: {
|
|
16
|
+
secret: string;
|
|
17
|
+
ttlMs?: number;
|
|
18
|
+
}): RoomTokenSigner;
|
|
19
|
+
//# sourceMappingURL=room-token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"room-token.d.ts","sourceRoot":"","sources":["../../src/rooms/room-token.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3C,0EAA0E;IAC1E,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,yBAAyB,QAAiB,CAAC;AACxD,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAE/C,wBAAgB,qBAAqB,CAAC,OAAO,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,eAAe,CA2ClG"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Room tokens: a signed statement that one user may join one room, until a
|
|
3
|
+
* time. HMAC-SHA256 over the JSON payload, `base64url(payload + "." + hexsig)`.
|
|
4
|
+
*
|
|
5
|
+
* The server signs a token only when the product's API asks for it
|
|
6
|
+
* (`POST /internal/room-token`, internal key), and the product asks only after
|
|
7
|
+
* its own authorization accepted the user for that resource. The server
|
|
8
|
+
* checks the token on join, against the joining socket's verified identity.
|
|
9
|
+
*/
|
|
10
|
+
import crypto from 'node:crypto';
|
|
11
|
+
export const DEFAULT_ROOM_TOKEN_TTL_MS = 15 * 60 * 1000;
|
|
12
|
+
export const MIN_ROOM_TOKEN_SECRET_LENGTH = 32;
|
|
13
|
+
export function createRoomTokenSigner(options) {
|
|
14
|
+
const { secret } = options;
|
|
15
|
+
if (typeof secret !== 'string' || secret.length < MIN_ROOM_TOKEN_SECRET_LENGTH) {
|
|
16
|
+
throw new Error(`[agent-sdk-realtime] roomTokens.secret must be at least ${MIN_ROOM_TOKEN_SECRET_LENGTH} characters: it signs every room token`);
|
|
17
|
+
}
|
|
18
|
+
const ttlMs = options.ttlMs ?? DEFAULT_ROOM_TOKEN_TTL_MS;
|
|
19
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0)
|
|
20
|
+
throw new Error('[agent-sdk-realtime] roomTokens.ttlMs must be a positive number');
|
|
21
|
+
const hmac = (payload) => crypto.createHmac('sha256', secret).update(payload).digest('hex');
|
|
22
|
+
return {
|
|
23
|
+
ttlMs,
|
|
24
|
+
sign(userId, room) {
|
|
25
|
+
const payload = JSON.stringify({ userId, room, exp: Date.now() + ttlMs });
|
|
26
|
+
return Buffer.from(`${payload}.${hmac(payload)}`).toString('base64url');
|
|
27
|
+
},
|
|
28
|
+
verify(token) {
|
|
29
|
+
try {
|
|
30
|
+
const decoded = Buffer.from(token, 'base64url').toString('utf-8');
|
|
31
|
+
const dot = decoded.lastIndexOf('.');
|
|
32
|
+
if (dot === -1)
|
|
33
|
+
return null;
|
|
34
|
+
const payloadStr = decoded.slice(0, dot);
|
|
35
|
+
const sig = decoded.slice(dot + 1);
|
|
36
|
+
const expected = hmac(payloadStr);
|
|
37
|
+
// Constant-time: both are hex strings of one length when valid.
|
|
38
|
+
if (sig.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const payload = JSON.parse(payloadStr);
|
|
42
|
+
if (typeof payload.userId !== 'string' || typeof payload.room !== 'string' || typeof payload.exp !== 'number') {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (Date.now() > payload.exp)
|
|
46
|
+
return null;
|
|
47
|
+
return payload;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=room-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"room-token.js","sourceRoot":"","sources":["../../src/rooms/room-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,MAAM,MAAM,aAAa,CAAC;AAgBjC,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AACxD,MAAM,CAAC,MAAM,4BAA4B,GAAG,EAAE,CAAC;AAE/C,MAAM,UAAU,qBAAqB,CAAC,OAA2C;IAC/E,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAC3B,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,GAAG,4BAA4B,EAAE,CAAC;QAC/E,MAAM,IAAI,KAAK,CACb,2DAA2D,4BAA4B,wCAAwC,CAChI,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,yBAAyB,CAAC;IACzD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAC;IAE9H,MAAM,IAAI,GAAG,CAAC,OAAe,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAEpG,OAAO;QACL,KAAK;QAEL,IAAI,CAAC,MAAM,EAAE,IAAI;YACf,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAA6B,CAAC,CAAC;YACrG,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QAC1E,CAAC;QAED,MAAM,CAAC,KAAK;YACV,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;gBAClE,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;gBACrC,IAAI,GAAG,KAAK,CAAC,CAAC;oBAAE,OAAO,IAAI,CAAC;gBAC5B,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;gBACzC,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;gBACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC;gBAClC,gEAAgE;gBAChE,IAAI,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;oBACvG,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAqB,CAAC;gBAC3D,IAAI,OAAO,OAAO,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;oBAC9G,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG;oBAAE,OAAO,IAAI,CAAC;gBAC1C,OAAO,OAAO,CAAC;YACjB,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The realtime server: one implementation for every product (ADR-0227 §2.1).
|
|
3
|
+
*
|
|
4
|
+
* It carries turn events and the product's declared events to rooms, holds
|
|
5
|
+
* UI action results for the agent worker (ADR-0209), keeps the approvals an
|
|
6
|
+
* irreversible action waits on (ADR-0228), signs and checks room tokens, and
|
|
7
|
+
* accepts only declared client events. The product supplies the
|
|
8
|
+
* seams (types.ts); nothing here belongs to one product.
|
|
9
|
+
*/
|
|
10
|
+
import http from 'node:http';
|
|
11
|
+
import { Server } from 'socket.io';
|
|
12
|
+
import type { RealtimeServerConfig } from './types.js';
|
|
13
|
+
import { type RoomTokenSigner } from './rooms/room-token.js';
|
|
14
|
+
import { type OUIResultStore } from './oui/results.js';
|
|
15
|
+
import { type ApprovalStore } from './approvals/store.js';
|
|
16
|
+
import { type SettlementStore } from './events/settlements.js';
|
|
17
|
+
export interface RealtimeServerInstance {
|
|
18
|
+
io: Server;
|
|
19
|
+
httpServer: http.Server;
|
|
20
|
+
/** The port the server listens on (the one picked, when configured with 0). */
|
|
21
|
+
port: number;
|
|
22
|
+
/** Signs and checks room tokens with the configured secret. */
|
|
23
|
+
roomTokens: RoomTokenSigner;
|
|
24
|
+
/** The UI action result store. */
|
|
25
|
+
results: OUIResultStore;
|
|
26
|
+
/** The approval store (ADR-0228). */
|
|
27
|
+
approvals: ApprovalStore;
|
|
28
|
+
/** Job settlements, when the server has event declarations. */
|
|
29
|
+
settlements: SettlementStore | null;
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
export declare function createRealtimeServer(config: RealtimeServerConfig): Promise<RealtimeServerInstance>;
|
|
33
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,IAAI,MAAM,WAAW,CAAC;AAK7B,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAGnC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAKvD,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACpF,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAE7E,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAI/E,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAWtF,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC;IACxB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,UAAU,EAAE,eAAe,CAAC;IAC5B,kCAAkC;IAClC,OAAO,EAAE,cAAc,CAAC;IACxB,qCAAqC;IACrC,SAAS,EAAE,aAAa,CAAC;IACzB,+DAA+D;IAC/D,WAAW,EAAE,eAAe,GAAG,IAAI,CAAC;IACpC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAqBD,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAkIxG"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The realtime server: one implementation for every product (ADR-0227 §2.1).
|
|
3
|
+
*
|
|
4
|
+
* It carries turn events and the product's declared events to rooms, holds
|
|
5
|
+
* UI action results for the agent worker (ADR-0209), keeps the approvals an
|
|
6
|
+
* irreversible action waits on (ADR-0228), signs and checks room tokens, and
|
|
7
|
+
* accepts only declared client events. The product supplies the
|
|
8
|
+
* seams (types.ts); nothing here belongs to one product.
|
|
9
|
+
*/
|
|
10
|
+
import http from 'node:http';
|
|
11
|
+
import express from 'express';
|
|
12
|
+
import cors from 'cors';
|
|
13
|
+
import helmet from 'helmet';
|
|
14
|
+
import { Server } from 'socket.io';
|
|
15
|
+
import { Redis } from 'ioredis';
|
|
16
|
+
import { createAdapter } from '@socket.io/redis-adapter';
|
|
17
|
+
import { assertRealtimeServerConfig } from './config.js';
|
|
18
|
+
import { createConsoleLogger } from './logger.js';
|
|
19
|
+
import { registerClientEvents, assertUniqueClientEvents } from './client-events/chain.js';
|
|
20
|
+
import { createBuiltinClientEvents } from './client-events/builtin.js';
|
|
21
|
+
import { createRoomTokenSigner } from './rooms/room-token.js';
|
|
22
|
+
import { createOUIResultStore } from './oui/results.js';
|
|
23
|
+
import { createApprovalTokenSigner } from './approvals/token.js';
|
|
24
|
+
import { createApprovalStore } from './approvals/store.js';
|
|
25
|
+
import { createApprovalDecideEvent } from './approvals/client-event.js';
|
|
26
|
+
import { approvalRouter } from './approvals/routes.js';
|
|
27
|
+
import { createDeclaredEvents } from './events/declared.js';
|
|
28
|
+
import { createSettlementStore } from './events/settlements.js';
|
|
29
|
+
import { createInternalKeyCheck, emitRouter, healthRouter, ouiResultsRouter, roomTokenRouter, settlementsRouter, } from './http/routes.js';
|
|
30
|
+
function connectRedis(config, label, logger) {
|
|
31
|
+
const client = new Redis({
|
|
32
|
+
host: config.host,
|
|
33
|
+
port: config.port,
|
|
34
|
+
username: config.username,
|
|
35
|
+
password: config.password,
|
|
36
|
+
tls: config.tls ? {} : undefined,
|
|
37
|
+
maxRetriesPerRequest: null,
|
|
38
|
+
lazyConnect: true,
|
|
39
|
+
retryStrategy(times) {
|
|
40
|
+
const delay = Math.min(times * 500, 10_000);
|
|
41
|
+
logger.warn({ label, attempt: times, nextRetryMs: delay }, 'Redis retry');
|
|
42
|
+
return delay;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
client.on('error', (err) => logger.error({ err, label }, 'Redis client error'));
|
|
46
|
+
return client;
|
|
47
|
+
}
|
|
48
|
+
export async function createRealtimeServer(config) {
|
|
49
|
+
assertRealtimeServerConfig(config);
|
|
50
|
+
const logger = config.logger ?? createConsoleLogger();
|
|
51
|
+
const roomTokens = createRoomTokenSigner(config.roomTokens);
|
|
52
|
+
const approvalTokens = createApprovalTokenSigner(config.approvals);
|
|
53
|
+
// Compiled before anything connects: a payload schema that cannot compile
|
|
54
|
+
// fails start-up, naming the event.
|
|
55
|
+
const declared = config.events ? createDeclaredEvents(config.events) : null;
|
|
56
|
+
// Redis first: without it there is no result store and no fan-out, so the
|
|
57
|
+
// server does not accept connections it cannot serve.
|
|
58
|
+
const pub = connectRedis(config.redis, 'pub', logger);
|
|
59
|
+
const sub = connectRedis(config.redis, 'sub', logger);
|
|
60
|
+
// Subscriber mode takes over a connection, and `sub` belongs to the socket.io
|
|
61
|
+
// adapter, so UI action results get a subscriber of their own.
|
|
62
|
+
const resultsSub = connectRedis(config.redis, 'oui-results-sub', logger);
|
|
63
|
+
const redisClients = [pub, sub, resultsSub];
|
|
64
|
+
try {
|
|
65
|
+
await Promise.all(redisClients.map((c) => c.connect()));
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
redisClients.forEach((c) => c.disconnect());
|
|
69
|
+
throw new Error(`[agent-sdk-realtime] Redis at ${config.redis.host}:${config.redis.port} is unreachable: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
70
|
+
}
|
|
71
|
+
const results = createOUIResultStore(pub, resultsSub, logger);
|
|
72
|
+
const approvals = createApprovalStore(pub, approvalTokens, logger);
|
|
73
|
+
const settlements = declared ? createSettlementStore(pub, resultsSub, logger) : null;
|
|
74
|
+
const clientEvents = [
|
|
75
|
+
...createBuiltinClientEvents({
|
|
76
|
+
roomPolicy: config.roomPolicy,
|
|
77
|
+
roomTokens,
|
|
78
|
+
relay: config.relay ?? {},
|
|
79
|
+
declared,
|
|
80
|
+
results: () => results,
|
|
81
|
+
}),
|
|
82
|
+
createApprovalDecideEvent(() => approvals),
|
|
83
|
+
...(config.clientEvents ?? []),
|
|
84
|
+
];
|
|
85
|
+
assertUniqueClientEvents(clientEvents);
|
|
86
|
+
const app = express();
|
|
87
|
+
app.use(helmet());
|
|
88
|
+
app.use(cors({ origin: config.corsOrigins, credentials: true }));
|
|
89
|
+
app.use(express.json({ limit: '1mb' }));
|
|
90
|
+
const httpServer = http.createServer(app);
|
|
91
|
+
const io = new Server(httpServer, {
|
|
92
|
+
cors: { origin: config.corsOrigins, methods: ['GET', 'POST'], credentials: true },
|
|
93
|
+
pingInterval: config.pingInterval ?? 25_000,
|
|
94
|
+
pingTimeout: config.pingTimeout ?? 20_000,
|
|
95
|
+
});
|
|
96
|
+
io.adapter(createAdapter(pub, sub));
|
|
97
|
+
const deps = {
|
|
98
|
+
io,
|
|
99
|
+
roomPolicy: config.roomPolicy,
|
|
100
|
+
roomTokens,
|
|
101
|
+
results: () => results,
|
|
102
|
+
declared,
|
|
103
|
+
settlements: () => settlements,
|
|
104
|
+
isInternalKey: createInternalKeyCheck(config.internalApiKey),
|
|
105
|
+
logger,
|
|
106
|
+
};
|
|
107
|
+
app.use(healthRouter(deps));
|
|
108
|
+
app.use(emitRouter(deps));
|
|
109
|
+
app.use(roomTokenRouter(deps));
|
|
110
|
+
app.use(ouiResultsRouter(deps));
|
|
111
|
+
app.use(approvalRouter({ approvals, isInternalKey: deps.isInternalKey }));
|
|
112
|
+
app.use(settlementsRouter(deps));
|
|
113
|
+
// Authentication, once per socket, through the product's verifier.
|
|
114
|
+
io.use(async (socket, next) => {
|
|
115
|
+
const token = socket.handshake.auth?.token ||
|
|
116
|
+
socket.handshake.headers.authorization?.replace(/^Bearer\s+/i, '');
|
|
117
|
+
if (!token) {
|
|
118
|
+
next(new Error('Authentication required'));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const user = await config.auth.verify(token);
|
|
123
|
+
if (!user || typeof user.userId !== 'string' || !user.userId)
|
|
124
|
+
throw new Error('verifier returned no userId');
|
|
125
|
+
socket.data = { user };
|
|
126
|
+
next();
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
logger.warn({ err, socketId: socket.id }, 'Socket authentication failed');
|
|
130
|
+
next(new Error('Authentication failed'));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
io.on('connection', (raw) => {
|
|
134
|
+
const socket = raw;
|
|
135
|
+
const { user } = socket.data;
|
|
136
|
+
const identityRooms = config.roomPolicy.identityRooms(user);
|
|
137
|
+
for (const room of identityRooms)
|
|
138
|
+
socket.join(room);
|
|
139
|
+
logger.info({ socketId: socket.id, userId: user.userId, accountId: user.accountId, rooms: identityRooms }, 'Client connected');
|
|
140
|
+
registerClientEvents(io, socket, clientEvents, logger);
|
|
141
|
+
socket.on('disconnect', (reason) => {
|
|
142
|
+
logger.info({ socketId: socket.id, userId: user.userId, reason }, 'Client disconnected');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
await new Promise((resolve, reject) => {
|
|
146
|
+
httpServer.once('error', reject);
|
|
147
|
+
httpServer.listen(config.port, () => {
|
|
148
|
+
httpServer.off('error', reject);
|
|
149
|
+
resolve();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
const port = httpServer.address().port;
|
|
153
|
+
logger.info({ port, declaredEvents: config.events?.names().length ?? null }, 'Realtime server listening');
|
|
154
|
+
return {
|
|
155
|
+
io,
|
|
156
|
+
httpServer,
|
|
157
|
+
port,
|
|
158
|
+
roomTokens,
|
|
159
|
+
results,
|
|
160
|
+
approvals,
|
|
161
|
+
settlements,
|
|
162
|
+
async close() {
|
|
163
|
+
await new Promise((resolve) => io.close(() => resolve()));
|
|
164
|
+
await Promise.all(redisClients.map((c) => c.quit().catch(() => c.disconnect())));
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AA0BD,wBAAsB,cAAc,IAAI,OAAO,CAAC,SAAS,CAAC,CAqDzD"}
|