@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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -0
  3. package/dist/approvals/client-event.d.ts +4 -0
  4. package/dist/approvals/client-event.d.ts.map +1 -0
  5. package/dist/approvals/client-event.js +25 -0
  6. package/dist/approvals/client-event.js.map +1 -0
  7. package/dist/approvals/routes.d.ts +19 -0
  8. package/dist/approvals/routes.d.ts.map +1 -0
  9. package/dist/approvals/routes.js +100 -0
  10. package/dist/approvals/routes.js.map +1 -0
  11. package/dist/approvals/schemas.d.ts +7 -0
  12. package/dist/approvals/schemas.d.ts.map +1 -0
  13. package/dist/approvals/schemas.js +45 -0
  14. package/dist/approvals/schemas.js.map +1 -0
  15. package/dist/approvals/store.d.ts +33 -0
  16. package/dist/approvals/store.d.ts.map +1 -0
  17. package/dist/approvals/store.js +206 -0
  18. package/dist/approvals/store.js.map +1 -0
  19. package/dist/approvals/token.d.ts +19 -0
  20. package/dist/approvals/token.d.ts.map +1 -0
  21. package/dist/approvals/token.js +73 -0
  22. package/dist/approvals/token.js.map +1 -0
  23. package/dist/client-events/builtin.d.ts +30 -0
  24. package/dist/client-events/builtin.d.ts.map +1 -0
  25. package/dist/client-events/builtin.js +161 -0
  26. package/dist/client-events/builtin.js.map +1 -0
  27. package/dist/client-events/chain.d.ts +80 -0
  28. package/dist/client-events/chain.d.ts.map +1 -0
  29. package/dist/client-events/chain.js +92 -0
  30. package/dist/client-events/chain.js.map +1 -0
  31. package/dist/config.d.ts +4 -0
  32. package/dist/config.d.ts.map +1 -0
  33. package/dist/config.js +85 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/events/declared.d.ts +18 -0
  36. package/dist/events/declared.d.ts.map +1 -0
  37. package/dist/events/declared.js +30 -0
  38. package/dist/events/declared.js.map +1 -0
  39. package/dist/events/settlements.d.ts +28 -0
  40. package/dist/events/settlements.d.ts.map +1 -0
  41. package/dist/events/settlements.js +29 -0
  42. package/dist/events/settlements.js.map +1 -0
  43. package/dist/http/routes.d.ts +91 -0
  44. package/dist/http/routes.d.ts.map +1 -0
  45. package/dist/http/routes.js +290 -0
  46. package/dist/http/routes.js.map +1 -0
  47. package/dist/index.d.ts +34 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +24 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/logger.d.ts +14 -0
  52. package/dist/logger.d.ts.map +1 -0
  53. package/dist/logger.js +22 -0
  54. package/dist/logger.js.map +1 -0
  55. package/dist/oui/results.d.ts +46 -0
  56. package/dist/oui/results.d.ts.map +1 -0
  57. package/dist/oui/results.js +56 -0
  58. package/dist/oui/results.js.map +1 -0
  59. package/dist/redis-waits.d.ts +32 -0
  60. package/dist/redis-waits.d.ts.map +1 -0
  61. package/dist/redis-waits.js +62 -0
  62. package/dist/redis-waits.js.map +1 -0
  63. package/dist/rooms/join.d.ts +13 -0
  64. package/dist/rooms/join.d.ts.map +1 -0
  65. package/dist/rooms/join.js +27 -0
  66. package/dist/rooms/join.js.map +1 -0
  67. package/dist/rooms/room-token.d.ts +19 -0
  68. package/dist/rooms/room-token.d.ts.map +1 -0
  69. package/dist/rooms/room-token.js +55 -0
  70. package/dist/rooms/room-token.js.map +1 -0
  71. package/dist/server.d.ts +33 -0
  72. package/dist/server.d.ts.map +1 -0
  73. package/dist/server.js +168 -0
  74. package/dist/server.js.map +1 -0
  75. package/dist/testing/index.d.ts +7 -0
  76. package/dist/testing/index.d.ts.map +1 -0
  77. package/dist/testing/index.js +87 -0
  78. package/dist/testing/index.js.map +1 -0
  79. package/dist/types.d.ts +135 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/types.js +2 -0
  82. package/dist/types.js.map +1 -0
  83. package/package.json +62 -0
@@ -0,0 +1,19 @@
1
+ import { type ApprovalTokenClaims } from '@ouispec/agent-core';
2
+ export declare const MIN_APPROVAL_KEY_LENGTH = 32;
3
+ export type ApprovalTokenCheck = {
4
+ ok: true;
5
+ claims: ApprovalTokenClaims;
6
+ } | {
7
+ ok: false;
8
+ reason: 'invalid' | 'expired';
9
+ error: string;
10
+ };
11
+ export interface ApprovalTokenSigner {
12
+ sign(claims: ApprovalTokenClaims): string;
13
+ /** The claims of a token this key signed, still unexpired; otherwise why not. */
14
+ verify(token: string, nowMs?: number): ApprovalTokenCheck;
15
+ }
16
+ export declare function createApprovalTokenSigner(options: {
17
+ signingKey: string;
18
+ }): ApprovalTokenSigner;
19
+ //# sourceMappingURL=token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../src/approvals/token.ts"],"names":[],"mappings":"AAUA,OAAO,EAAwC,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAErG,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAI1C,MAAM,MAAM,kBAAkB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzI,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,CAAC;IAC1C,iFAAiF;IACjF,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAAC;CAC3D;AAYD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,mBAAmB,CAwC9F"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The approval token (ADR-0228 §2.3): a compact JWS, HS256, signed with the
3
+ * environment's approval key. It is ADR-0210's `commit_token`: an engine in
4
+ * another language verifies it with the same key.
5
+ *
6
+ * Only the approval store signs one, and only when the approval's own user
7
+ * decided it. A token proves that decision; the store's record (single use,
8
+ * the stored call) is what redemption checks it against.
9
+ */
10
+ import crypto from 'node:crypto';
11
+ import { APPROVAL_CHANNELS, ARGS_HASH_PATTERN } from '@ouispec/agent-core';
12
+ export const MIN_APPROVAL_KEY_LENGTH = 32;
13
+ const HEADER = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
14
+ const STRING_CLAIMS = ['aid', 'sub', 'cid', 'tool', 'eff', 'jti'];
15
+ function claimsProblem(c) {
16
+ for (const k of STRING_CLAIMS)
17
+ if (typeof c[k] !== 'string' || !c[k])
18
+ return `claim ${k} is missing`;
19
+ if (typeof c.ah !== 'string' || !ARGS_HASH_PATTERN.test(c.ah))
20
+ return 'claim ah is not an args hash';
21
+ if (!APPROVAL_CHANNELS.includes(c.ch))
22
+ return 'claim ch is not a channel';
23
+ for (const k of ['iat', 'exp'])
24
+ if (!Number.isInteger(c[k]))
25
+ return `claim ${k} is not a time`;
26
+ return null;
27
+ }
28
+ export function createApprovalTokenSigner(options) {
29
+ const { signingKey } = options;
30
+ if (typeof signingKey !== 'string' || signingKey.length < MIN_APPROVAL_KEY_LENGTH) {
31
+ throw new Error(`[agent-sdk-realtime] approvals.signingKey must be at least ${MIN_APPROVAL_KEY_LENGTH} characters: it signs every approval token`);
32
+ }
33
+ const mac = (input) => crypto.createHmac('sha256', signingKey).update(input).digest();
34
+ return {
35
+ sign(claims) {
36
+ const problem = claimsProblem(claims);
37
+ if (problem)
38
+ throw new Error(`[agent-sdk-realtime] Cannot sign an approval token: ${problem}`);
39
+ const signingInput = `${HEADER}.${Buffer.from(JSON.stringify(claims)).toString('base64url')}`;
40
+ return `${signingInput}.${mac(signingInput).toString('base64url')}`;
41
+ },
42
+ verify(token, nowMs = Date.now()) {
43
+ const invalid = (error) => ({ ok: false, reason: 'invalid', error });
44
+ if (typeof token !== 'string')
45
+ return invalid('not a token');
46
+ const parts = token.split('.');
47
+ if (parts.length !== 3)
48
+ return invalid('not a compact JWS');
49
+ const [header, payload, signature] = parts;
50
+ // Only this header: no `alg` a token could choose for itself, `none` included.
51
+ if (header !== HEADER)
52
+ return invalid('not an HS256 approval token');
53
+ const presented = Buffer.from(signature, 'base64url');
54
+ const expected = mac(`${header}.${payload}`);
55
+ if (presented.length !== expected.length || !crypto.timingSafeEqual(presented, expected))
56
+ return invalid('bad signature');
57
+ let claims;
58
+ try {
59
+ claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
60
+ }
61
+ catch {
62
+ return invalid('unreadable claims');
63
+ }
64
+ const problem = claimsProblem(claims);
65
+ if (problem)
66
+ return invalid(problem);
67
+ if (nowMs >= claims.exp * 1000)
68
+ return { ok: false, reason: 'expired', error: 'the approval has expired' };
69
+ return { ok: true, claims: claims };
70
+ },
71
+ };
72
+ }
73
+ //# sourceMappingURL=token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.js","sourceRoot":"","sources":["../../src/approvals/token.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import { type ClientEvent } from './chain.js';
2
+ import type { RoomTokenSigner } from '../rooms/room-token.js';
3
+ import type { OUIResultStore } from '../oui/results.js';
4
+ import type { RelayRule, RoomPolicy } from '../types.js';
5
+ import { type DeclaredEvents } from '../events/declared.js';
6
+ export declare const MAX_RELAY_ROOMS = 5;
7
+ export interface BuiltinClientEventDeps {
8
+ roomPolicy: RoomPolicy;
9
+ roomTokens: RoomTokenSigner;
10
+ relay: Readonly<Record<string, RelayRule>>;
11
+ /** The product's event declarations, when given: a relayed event is held to its declaration. */
12
+ declared: DeclaredEvents | null;
13
+ /** Resolved on use: the store exists once Redis is connected. */
14
+ results: () => OUIResultStore;
15
+ }
16
+ interface RelayPayload {
17
+ event: string;
18
+ data: unknown;
19
+ rooms: string[];
20
+ }
21
+ /**
22
+ * Why a relay is refused, or null when it is allowed: only an allow-listed
23
+ * event, with valid data, into rooms of its kind that this socket has joined
24
+ * (joining is where access to the resource is checked). With declarations,
25
+ * the event's declaration decides its rooms and data as well as the rule.
26
+ */
27
+ export declare function relayRefusal(relay: Readonly<Record<string, RelayRule>>, socketRooms: ReadonlySet<string>, payload: RelayPayload, declared?: DeclaredEvents | null): string | null;
28
+ export declare function createBuiltinClientEvents(deps: BuiltinClientEventDeps): ClientEvent[];
29
+ export {};
30
+ //# sourceMappingURL=builtin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builtin.d.ts","sourceRoot":"","sources":["../../src/client-events/builtin.ts"],"names":[],"mappings":"AAOA,OAAO,EAAqB,KAAK,WAAW,EAA2B,MAAM,YAAY,CAAC;AAE1F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAI7E,eAAO,MAAM,eAAe,IAAI,CAAC;AAEjC,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,UAAU,CAAC;IACvB,UAAU,EAAE,eAAe,CAAC;IAC5B,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IAC3C,gGAAgG;IAChG,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAC;IAChC,iEAAiE;IACjE,OAAO,EAAE,MAAM,cAAc,CAAC;CAC/B;AAUD,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,EAC1C,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,EAChC,OAAO,EAAE,YAAY,EACrB,QAAQ,GAAE,cAAc,GAAG,IAAW,GACrC,MAAM,GAAG,IAAI,CAgBf;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,sBAAsB,GAAG,WAAW,EAAE,CAuHrF"}
@@ -0,0 +1,161 @@
1
+ /**
2
+ * The client events every realtime server accepts: joining and leaving rooms,
3
+ * relaying an allow-listed event, and answering a UI action (ADR-0209). A
4
+ * product declares its own events beside these (`config.clientEvents`).
5
+ */
6
+ import Joi from 'joi';
7
+ import { defineClientEvent } from './chain.js';
8
+ import { joinRefusal } from '../rooms/join.js';
9
+ import { OUI_WIRE } from '@ouispec/agent-events';
10
+ import { declaredRefusal } from '../events/declared.js';
11
+ // A client re-sends every room it holds on reconnect, in one subscribe.
12
+ const MAX_ROOMS = 100;
13
+ export const MAX_RELAY_ROOMS = 5;
14
+ function normalizeSubscribe(payload) {
15
+ if (typeof payload === 'string')
16
+ return { rooms: [payload], tokens: {} };
17
+ if (Array.isArray(payload))
18
+ return { rooms: payload, tokens: {} };
19
+ return { rooms: Array.isArray(payload.rooms) ? payload.rooms : [payload.rooms], tokens: payload.tokens ?? {} };
20
+ }
21
+ /**
22
+ * Why a relay is refused, or null when it is allowed: only an allow-listed
23
+ * event, with valid data, into rooms of its kind that this socket has joined
24
+ * (joining is where access to the resource is checked). With declarations,
25
+ * the event's declaration decides its rooms and data as well as the rule.
26
+ */
27
+ export function relayRefusal(relay, socketRooms, payload, declared = null) {
28
+ const rule = relay[payload.event];
29
+ if (!rule)
30
+ return `event "${payload.event}" may not be relayed by a client`;
31
+ if (declared) {
32
+ const refusal = declaredRefusal(declared, payload.event, payload.data, payload.rooms, 'data');
33
+ if (refusal)
34
+ return refusal;
35
+ }
36
+ if (rule.data) {
37
+ const { error } = rule.data.validate(payload.data, { convert: false });
38
+ if (error)
39
+ return `invalid "${payload.event}" data: ${error.message}`;
40
+ }
41
+ for (const room of payload.rooms) {
42
+ if (rule.roomPrefix && !room.startsWith(`${rule.roomPrefix}:`))
43
+ return `"${payload.event}" may only go to ${rule.roomPrefix}: rooms`;
44
+ if (!socketRooms.has(room))
45
+ return `not a member of ${room}`;
46
+ }
47
+ return null;
48
+ }
49
+ export function createBuiltinClientEvents(deps) {
50
+ const { roomPolicy, roomTokens, relay } = deps;
51
+ const room = Joi.string()
52
+ .max(200)
53
+ .custom((value, helpers) => (roomPolicy.isValidRoom(value) ? value : helpers.error('any.invalid')));
54
+ // ─── subscribe ─────────────────────────────────────────────────────────────
55
+ // The bare forms (`'room'`, `['room']`) and the token-bearing form
56
+ // (`{ rooms, tokens }`). Each room is judged on its own, so one refused room
57
+ // does not refuse the others; the ack names each.
58
+ const subscribe = defineClientEvent({
59
+ name: 'subscribe',
60
+ schema: Joi.alternatives().try(room.required(), Joi.array().items(room.required()).min(1).max(MAX_ROOMS), Joi.object({
61
+ rooms: Joi.alternatives().try(room.required(), Joi.array().items(room.required()).min(1).max(MAX_ROOMS)).required(),
62
+ tokens: Joi.object().pattern(room, Joi.string().max(2048)).default({}),
63
+ })),
64
+ maxBytes: 64 * 1024,
65
+ rate: { perSecond: 10, burst: 40 },
66
+ handle: ({ socket, user, logger }, payload, ack) => {
67
+ const { rooms, tokens } = normalizeSubscribe(payload);
68
+ const joined = [];
69
+ const denied = [];
70
+ const reasons = {};
71
+ for (const r of rooms) {
72
+ const refusal = joinRefusal(roomPolicy, roomTokens, r, user, tokens[r]);
73
+ if (refusal) {
74
+ denied.push(r);
75
+ reasons[r] = refusal;
76
+ }
77
+ else {
78
+ socket.join(r);
79
+ joined.push(r);
80
+ }
81
+ }
82
+ if (denied.length > 0)
83
+ logger.warn({ socketId: socket.id, userId: user.userId, denied: reasons }, 'Room subscription denied');
84
+ if (joined.length > 0)
85
+ logger.info({ socketId: socket.id, userId: user.userId, joined }, 'Subscribed to rooms');
86
+ ack?.({ ok: denied.length === 0, joined, denied: denied.length > 0 ? denied : undefined });
87
+ },
88
+ });
89
+ // ─── unsubscribe ───────────────────────────────────────────────────────────
90
+ const unsubscribe = defineClientEvent({
91
+ name: 'unsubscribe',
92
+ schema: Joi.alternatives().try(room.required(), Joi.array().items(room.required()).min(1).max(MAX_ROOMS)),
93
+ maxBytes: 16 * 1024,
94
+ rate: { perSecond: 10, burst: 40 },
95
+ handle: ({ socket, user, logger }, payload, ack) => {
96
+ const rooms = Array.isArray(payload) ? payload : [payload];
97
+ // The rooms a socket is placed in by its identity cannot be left.
98
+ const fixed = new Set(roomPolicy.identityRooms(user));
99
+ for (const r of rooms)
100
+ if (!fixed.has(r))
101
+ socket.leave(r);
102
+ logger.info({ socketId: socket.id, rooms }, 'Unsubscribed from rooms');
103
+ ack?.({ ok: true });
104
+ },
105
+ });
106
+ // ─── relay ─────────────────────────────────────────────────────────────────
107
+ const relayEvent = defineClientEvent({
108
+ name: 'relay',
109
+ schema: Joi.object({
110
+ event: Joi.string().max(100).required(),
111
+ data: Joi.any(),
112
+ rooms: Joi.array().items(room.required()).min(1).max(MAX_RELAY_ROOMS).required(),
113
+ }),
114
+ maxBytes: 8 * 1024,
115
+ rate: { perSecond: 5, burst: 20 },
116
+ authorize: ({ socket }, payload) => {
117
+ const refusal = relayRefusal(relay, socket.rooms, payload, deps.declared);
118
+ return refusal ? { ok: false, reason: refusal } : { ok: true };
119
+ },
120
+ handle: ({ io, socket, user, logger }, payload, ack) => {
121
+ // To every member, the sender included: a tab updates its own view from
122
+ // the relayed event, as every other tab does.
123
+ io.to(payload.rooms).emit(payload.event, payload.data);
124
+ logger.debug({ socketId: socket.id, userId: user.userId, event: payload.event, rooms: payload.rooms }, 'Relayed event');
125
+ ack?.({ ok: true });
126
+ },
127
+ });
128
+ // ─── OUI action result (ADR-0209) ──────────────────────────────────────────
129
+ const ouiActionResult = defineClientEvent({
130
+ name: OUI_WIRE.result,
131
+ schema: Joi.object({
132
+ requestId: Joi.string().max(200).required(),
133
+ success: Joi.boolean().required(),
134
+ }).unknown(true),
135
+ maxBytes: 512 * 1024,
136
+ rate: { perSecond: 20, burst: 60 },
137
+ // Scoped by construction: the answer is kept under this socket's user, and
138
+ // the worker only ever collects answers for the turn's own user.
139
+ //
140
+ // Acknowledged either way (oui-spec §7.3.6): a refusal — too large,
141
+ // invalid, over the rate — is acknowledged by the chain with its reason, so
142
+ // the tab sends the answer again trimmed instead of the worker waiting out
143
+ // its deadline for an answer that was dropped.
144
+ handle: async ({ socket, user, logger }, result, ack) => {
145
+ const kept = await deps.results().store(user.userId, result);
146
+ if (kept) {
147
+ logger.info({
148
+ socketId: socket.id,
149
+ userId: user.userId,
150
+ requestId: result.requestId,
151
+ success: result.success,
152
+ interim: result.interim ?? false,
153
+ ...(result.delivery ? { trimmed: result.delivery.omitted, refusedBecause: result.delivery.reason } : {}),
154
+ }, 'OUI result received');
155
+ }
156
+ ack?.({ ok: true, kept });
157
+ },
158
+ });
159
+ return [subscribe, unsubscribe, relayEvent, ouiActionResult];
160
+ }
161
+ //# sourceMappingURL=builtin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builtin.js","sourceRoot":"","sources":["../../src/client-events/builtin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Client events: what a browser may send the server, declared and guarded.
3
+ *
4
+ * An HTTP API never runs a handler that is not declared: every route has a
5
+ * schema and a chain of middleware (authentication, account, permission) in
6
+ * front of it. A bare `socket.on(...)` has none of that, and trusts its
7
+ * payload. That is how a `relay` handler once sent whatever event a client
8
+ * named to whatever room it named, so any signed-in user could send
9
+ * `oui:dispatch` into another account's tabs (ADR-0227, W0).
10
+ *
11
+ * Every client event goes through one chain, the socket counterpart of a
12
+ * route module:
13
+ *
14
+ * 1. declared — an event with no definition is refused and logged;
15
+ * 2. rate limit — per socket, per event (token bucket);
16
+ * 3. size, then schema — bounded bytes of JSON, then the event's schema;
17
+ * 4. authorize — the definition's own rule, against the socket's verified
18
+ * identity (never an identity the payload claims);
19
+ * 5. handle.
20
+ *
21
+ * Authentication happens once, at connection, through the product's
22
+ * `AuthAdapter`.
23
+ */
24
+ import type { Server, Socket } from 'socket.io';
25
+ import type { AuthResult, PayloadSchema } from '../types.js';
26
+ import type { RealtimeLogger } from '../logger.js';
27
+ export interface AuthenticatedSocket extends Socket {
28
+ data: {
29
+ user: AuthResult;
30
+ };
31
+ }
32
+ export interface ClientEventContext {
33
+ io: Server;
34
+ socket: AuthenticatedSocket;
35
+ user: AuthResult;
36
+ logger: RealtimeLogger;
37
+ }
38
+ export type Authorization = {
39
+ ok: true;
40
+ } | {
41
+ ok: false;
42
+ reason: string;
43
+ };
44
+ export type Ack = (response: unknown) => void;
45
+ export interface ClientEventDefinition<P = unknown> {
46
+ /** The event name the client emits. */
47
+ name: string;
48
+ /** Validates the payload. Unknown keys should be refused. */
49
+ schema: PayloadSchema;
50
+ /** Largest payload accepted, in bytes of JSON. */
51
+ maxBytes: number;
52
+ /** Token bucket per socket: sustained events per second, and burst. */
53
+ rate: {
54
+ perSecond: number;
55
+ burst: number;
56
+ };
57
+ /** May this socket's user do this, with this payload? Omit only when the handler scopes itself to the user. */
58
+ authorize?: (ctx: ClientEventContext, payload: P) => Authorization;
59
+ /** What a refusal is answered with, when the client passed an ack. */
60
+ refusal?: (reason: string) => unknown;
61
+ handle: (ctx: ClientEventContext, payload: P, ack?: Ack) => void | Promise<void>;
62
+ }
63
+ /** A declared event, with its payload type sealed inside: what the connection handler attaches. */
64
+ export interface ClientEvent {
65
+ readonly name: string;
66
+ /** The guarded socket.io listener for one socket. */
67
+ listener(ctx: ClientEventContext): (raw: unknown, ack?: unknown) => void;
68
+ }
69
+ /** Declares a client event; the generic keeps `schema`, `authorize` and `handle` in step. */
70
+ export declare function defineClientEvent<P>(definition: ClientEventDefinition<P>): ClientEvent & {
71
+ definition: ClientEventDefinition<P>;
72
+ };
73
+ /** Refuses two declarations of one name: which one would run is otherwise an accident of order. */
74
+ export declare function assertUniqueClientEvents(events: readonly ClientEvent[]): void;
75
+ /**
76
+ * Attaches the declared events to a connected socket. Anything else the client
77
+ * sends is refused and logged, so a probe for an undeclared event is visible.
78
+ */
79
+ export declare function registerClientEvents(io: Server, socket: AuthenticatedSocket, events: readonly ClientEvent[], logger: RealtimeLogger): void;
80
+ //# sourceMappingURL=chain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chain.d.ts","sourceRoot":"","sources":["../../src/client-events/chain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,WAAW,mBAAoB,SAAQ,MAAM;IACjD,IAAI,EAAE;QAAE,IAAI,EAAE,UAAU,CAAA;KAAE,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,mBAAmB,CAAC;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzE,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;AAE9C,MAAM,WAAW,qBAAqB,CAAC,CAAC,GAAG,OAAO;IAChD,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,MAAM,EAAE,aAAa,CAAC;IACtB,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,+GAA+G;IAC/G,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,EAAE,OAAO,EAAE,CAAC,KAAK,aAAa,CAAC;IACnE,sEAAsE;IACtE,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IACtC,MAAM,EAAE,CAAC,GAAG,EAAE,kBAAkB,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClF;AAED,mGAAmG;AACnG,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,QAAQ,CAAC,GAAG,EAAE,kBAAkB,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CAC1E;AAED,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,UAAU,EAAE,qBAAqB,CAAC,CAAC,CAAC,GACnC,WAAW,GAAG;IAAE,UAAU,EAAE,qBAAqB,CAAC,CAAC,CAAC,CAAA;CAAE,CAExD;AAkED,mGAAmG;AACnG,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,GAAG,IAAI,CAM7E;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,mBAAmB,EAC3B,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,MAAM,EAAE,cAAc,GACrB,IAAI,CAcN"}
@@ -0,0 +1,92 @@
1
+ /** Declares a client event; the generic keeps `schema`, `authorize` and `handle` in step. */
2
+ export function defineClientEvent(definition) {
3
+ return { name: definition.name, definition, listener: (ctx) => guard(ctx, definition) };
4
+ }
5
+ // ─── Rate limiting ───────────────────────────────────────────────────────────
6
+ class TokenBucket {
7
+ perSecond;
8
+ burst;
9
+ tokens;
10
+ last = Date.now();
11
+ constructor(perSecond, burst) {
12
+ this.perSecond = perSecond;
13
+ this.burst = burst;
14
+ this.tokens = burst;
15
+ }
16
+ take() {
17
+ const now = Date.now();
18
+ this.tokens = Math.min(this.burst, this.tokens + ((now - this.last) / 1000) * this.perSecond);
19
+ this.last = now;
20
+ if (this.tokens < 1)
21
+ return false;
22
+ this.tokens -= 1;
23
+ return true;
24
+ }
25
+ }
26
+ // ─── The chain ───────────────────────────────────────────────────────────────
27
+ function payloadBytes(payload) {
28
+ try {
29
+ return Buffer.byteLength(JSON.stringify(payload ?? null));
30
+ }
31
+ catch {
32
+ return Number.POSITIVE_INFINITY;
33
+ }
34
+ }
35
+ /** Runs one event through the chain and returns a handler socket.io can call. */
36
+ function guard(ctx, definition) {
37
+ const bucket = new TokenBucket(definition.rate.perSecond, definition.rate.burst);
38
+ const { socket, user, logger } = ctx;
39
+ const refuse = (reason, ack) => {
40
+ logger.warn({ socketId: socket.id, userId: user.userId, accountId: user.accountId, event: definition.name, reason }, 'Client event refused');
41
+ ack?.(definition.refusal ? definition.refusal(reason) : { ok: false, error: reason });
42
+ };
43
+ return (raw, maybeAck) => {
44
+ const ack = typeof maybeAck === 'function' ? maybeAck : undefined;
45
+ if (!bucket.take())
46
+ return refuse('rate limit exceeded', ack);
47
+ if (payloadBytes(raw) > definition.maxBytes)
48
+ return refuse(`payload larger than ${definition.maxBytes} bytes`, ack);
49
+ const { value, error } = definition.schema.validate(raw, { abortEarly: true, convert: false });
50
+ if (error)
51
+ return refuse(`invalid payload: ${error.message}`, ack);
52
+ if (definition.authorize) {
53
+ const decision = definition.authorize(ctx, value);
54
+ if (!decision.ok)
55
+ return refuse(decision.reason, ack);
56
+ }
57
+ Promise.resolve(definition.handle(ctx, value, ack)).catch((err) => {
58
+ logger.error({ err, socketId: socket.id, userId: user.userId, event: definition.name }, 'Client event handler failed');
59
+ ack?.({ ok: false, error: 'internal error' });
60
+ });
61
+ };
62
+ }
63
+ /** Refuses two declarations of one name: which one would run is otherwise an accident of order. */
64
+ export function assertUniqueClientEvents(events) {
65
+ const seen = new Set();
66
+ for (const event of events) {
67
+ if (seen.has(event.name))
68
+ throw new Error(`[agent-sdk-realtime] Client event "${event.name}" is declared twice`);
69
+ seen.add(event.name);
70
+ }
71
+ }
72
+ /**
73
+ * Attaches the declared events to a connected socket. Anything else the client
74
+ * sends is refused and logged, so a probe for an undeclared event is visible.
75
+ */
76
+ export function registerClientEvents(io, socket, events, logger) {
77
+ assertUniqueClientEvents(events);
78
+ const ctx = { io, socket, user: socket.data.user, logger };
79
+ const declared = new Set(events.map((e) => e.name));
80
+ for (const event of events)
81
+ socket.on(event.name, event.listener(ctx));
82
+ let undeclaredLogged = 0;
83
+ socket.onAny((event) => {
84
+ if (declared.has(event))
85
+ return;
86
+ // Bounded per socket: a flood of junk must not become a flood of logs.
87
+ if (undeclaredLogged++ < 20) {
88
+ logger.warn({ socketId: socket.id, userId: ctx.user.userId, event }, 'Undeclared client event refused');
89
+ }
90
+ });
91
+ }
92
+ //# sourceMappingURL=chain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chain.js","sourceRoot":"","sources":["../../src/client-events/chain.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { RealtimeServerConfig } from './types.js';
2
+ /** Throws, naming every missing or malformed seam, unless the configuration is complete. */
3
+ export declare function assertRealtimeServerConfig(config: Partial<RealtimeServerConfig>): asserts config is RealtimeServerConfig;
4
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAwCvD,4FAA4F;AAC5F,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,OAAO,CAAC,oBAAoB,CAAC,GAAG,OAAO,CAAC,MAAM,IAAI,oBAAoB,CAwBxH"}
package/dist/config.js ADDED
@@ -0,0 +1,85 @@
1
+ import { UndeclaredEventError } from '@ouispec/agent-events';
2
+ import { MIN_ROOM_TOKEN_SECRET_LENGTH } from './rooms/room-token.js';
3
+ import { MIN_APPROVAL_KEY_LENGTH } from './approvals/token.js';
4
+ /**
5
+ * Every seam the server cannot run without, and what it is for. A missing
6
+ * one fails start-up with this text: a default here would silently run some
7
+ * other product's value (ADR-0227 §2.2).
8
+ */
9
+ const REQUIRED = [
10
+ ['auth', (c) => typeof c.auth?.verify === 'function', "an AuthAdapter that verifies your users' tokens"],
11
+ [
12
+ 'roomPolicy',
13
+ (c) => !!c.roomPolicy &&
14
+ ['isValidRoom', 'identityRooms', 'requiresToken', 'canJoin'].every((m) => typeof c.roomPolicy?.[m] === 'function'),
15
+ 'a RoomPolicy (isValidRoom, identityRooms, requiresToken, canJoin)',
16
+ ],
17
+ ['internalApiKey', (c) => typeof c.internalApiKey === 'string' && c.internalApiKey.length > 0, 'the key that guards the internal endpoints'],
18
+ [
19
+ 'roomTokens.secret',
20
+ (c) => typeof c.roomTokens?.secret === 'string' && c.roomTokens.secret.length >= MIN_ROOM_TOKEN_SECRET_LENGTH,
21
+ `the room-token signing secret, at least ${MIN_ROOM_TOKEN_SECRET_LENGTH} characters`,
22
+ ],
23
+ [
24
+ 'approvals.signingKey',
25
+ (c) => typeof c.approvals?.signingKey === 'string' && c.approvals.signingKey.length >= MIN_APPROVAL_KEY_LENGTH,
26
+ `the approval-token signing key, at least ${MIN_APPROVAL_KEY_LENGTH} characters`,
27
+ ],
28
+ ['redis.host', (c) => typeof c.redis?.host === 'string' && c.redis.host.length > 0, 'where UI action results and room fan-out meet'],
29
+ ['redis.port', (c) => Number.isInteger(c.redis?.port) && (c.redis?.port ?? 0) > 0, 'the Redis port'],
30
+ ['redis.tls', (c) => typeof c.redis?.tls === 'boolean', 'whether Redis is reached over TLS'],
31
+ [
32
+ 'corsOrigins',
33
+ (c) => Array.isArray(c.corsOrigins) && c.corsOrigins.length > 0 && c.corsOrigins.every((o) => typeof o === 'string' && o),
34
+ "the browser origins allowed to connect (['*'] only deliberately)",
35
+ ],
36
+ ['port', (c) => Number.isInteger(c.port) && (c.port ?? -1) >= 0, 'the port to listen on (0 for any free port)'],
37
+ ];
38
+ /** Throws, naming every missing or malformed seam, unless the configuration is complete. */
39
+ export function assertRealtimeServerConfig(config) {
40
+ const missing = REQUIRED.filter(([, ok]) => !ok(config ?? {})).map(([name, , what]) => `${name} (${what})`);
41
+ if (missing.length > 0) {
42
+ throw new Error(`[agent-sdk-realtime] Missing required configuration: ${missing.join('; ')}`);
43
+ }
44
+ if (config.internalApiKey === config.roomTokens?.secret) {
45
+ throw new Error('[agent-sdk-realtime] roomTokens.secret must not be the internalApiKey: they are separate secrets');
46
+ }
47
+ const { approvals, roomTokens, internalApiKey } = config;
48
+ if (approvals.signingKey === internalApiKey || approvals.signingKey === roomTokens.secret) {
49
+ throw new Error('[agent-sdk-realtime] approvals.signingKey must be neither the internalApiKey nor roomTokens.secret: they are separate secrets');
50
+ }
51
+ for (const [event, rule] of Object.entries(config.relay ?? {})) {
52
+ if (config.events) {
53
+ assertRelayable(config.events, event);
54
+ if (rule?.data !== undefined && typeof rule.data.validate !== 'function') {
55
+ throw new Error(`[agent-sdk-realtime] relay["${event}"].data must be a schema with validate()`);
56
+ }
57
+ }
58
+ else if (!rule?.roomPrefix || typeof rule.data?.validate !== 'function') {
59
+ throw new Error(`[agent-sdk-realtime] relay["${event}"] needs a roomPrefix and a data schema`);
60
+ }
61
+ }
62
+ }
63
+ /**
64
+ * A client may relay only a declared notice that goes to the product's own
65
+ * rooms. A completion or failure relayed by a tab would forge a job's end, a
66
+ * progress event its state, and a turn's events belong to the worker.
67
+ */
68
+ function assertRelayable(events, name) {
69
+ let declared;
70
+ try {
71
+ declared = events.require(name, `relay["${name}"]`);
72
+ }
73
+ catch (err) {
74
+ if (err instanceof UndeclaredEventError)
75
+ throw new Error(err.message.replace('[agent-sdk-events]', '[agent-sdk-realtime]'), { cause: err });
76
+ throw err;
77
+ }
78
+ if (declared.role !== 'notice') {
79
+ throw new Error(`[agent-sdk-realtime] relay["${name}"]: a ${declared.role} event is sent only by the server; a client may relay only a notice`);
80
+ }
81
+ if (declared.rooms.some((r) => r.pattern === null)) {
82
+ throw new Error(`[agent-sdk-realtime] relay["${name}"]: it goes to a turn's room, which only the server sends to`);
83
+ }
84
+ }
85
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * What the server checks of a declared event, on emit and on relay: that it
3
+ * is declared, that its payload passes its schema, and that each room is one
4
+ * it is declared for. One reading of the declarations for both doors.
5
+ */
6
+ import type { EventCatalog } from '@ouispec/agent-events';
7
+ import { type PayloadValidator } from '@ouispec/agent-events/validate';
8
+ export interface DeclaredEvents {
9
+ catalog: EventCatalog;
10
+ validator: PayloadValidator;
11
+ }
12
+ export declare function createDeclaredEvents(catalog: EventCatalog): DeclaredEvents;
13
+ /**
14
+ * Why `data` may not be sent as `event` to `rooms`, or null when it may.
15
+ * `noun` names the data as the caller knows it: an emit's payload, a relay's data.
16
+ */
17
+ export declare function declaredRefusal(events: DeclaredEvents, event: string, data: unknown, rooms: readonly string[], noun: 'payload' | 'data'): string | null;
18
+ //# sourceMappingURL=declared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"declared.d.ts","sourceRoot":"","sources":["../../src/events/declared.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAE/F,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,YAAY,CAAC;IACtB,SAAS,EAAE,gBAAgB,CAAC;CAC7B;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG,cAAc,CAE1E;AAUD;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,IAAI,EAAE,SAAS,GAAG,MAAM,GACvB,MAAM,GAAG,IAAI,CAYf"}