@foony/realtime 0.0.1

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.
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Low-level WebSocket connection manager. Handles framing, request /
3
+ * response correlation, and dispatch to per-channel listeners.
4
+ *
5
+ * The class is intentionally protocol-aware but channel-agnostic — the
6
+ * Channel and Realtime classes layer the public API on top.
7
+ */
8
+ const DEFAULT_INITIAL_RECONNECT_DELAY_MS = 1_000;
9
+ const DEFAULT_MAX_RECONNECT_DELAY_MS = 30_000;
10
+ /** WebSocket.OPEN — duplicated here so we do not depend on a global. */
11
+ const READY_STATE_OPEN = 1;
12
+ /**
13
+ * Connection is the transport layer. One Realtime client owns one
14
+ * Connection; channels share it.
15
+ */
16
+ export class Connection {
17
+ options;
18
+ socket = null;
19
+ state = 'initialized';
20
+ connectionId = null;
21
+ serverClientId = null;
22
+ nextRequestId = 1;
23
+ pending = new Map();
24
+ channelListeners = new Map();
25
+ stateListeners = new Set();
26
+ connectPromise = null;
27
+ reconnectTimer = null;
28
+ reconnectAttempt = 0;
29
+ /** Channels the SDK has asked to be subscribed to; re-sent on reconnect. */
30
+ desiredSubscriptions = new Set();
31
+ constructor(options) {
32
+ const authMethods = Number(Boolean(options.token)) + Number(Boolean(options.authCallback)) + Number(Boolean(options.key));
33
+ if (authMethods !== 1) {
34
+ throw new Error('Connection: pass exactly one of options.token, options.authCallback, or options.key');
35
+ }
36
+ this.options = options;
37
+ }
38
+ /** Current connection state. */
39
+ getState() {
40
+ return this.state;
41
+ }
42
+ /** The server-issued connection id, populated after a successful auth handshake. */
43
+ getConnectionId() {
44
+ return this.connectionId;
45
+ }
46
+ /** The client id encoded in the token, populated after auth. */
47
+ getClientId() {
48
+ return this.serverClientId;
49
+ }
50
+ /** Register a state-change listener. Returns an unsubscribe function. */
51
+ onStateChange(listener) {
52
+ this.stateListeners.add(listener);
53
+ return () => this.stateListeners.delete(listener);
54
+ }
55
+ /**
56
+ * Open the WebSocket and complete the auth handshake. Idempotent —
57
+ * concurrent calls await the same in-flight connect.
58
+ */
59
+ async connect() {
60
+ if (this.state === 'connected')
61
+ return;
62
+ if (this.connectPromise)
63
+ return this.connectPromise;
64
+ this.connectPromise = this.doConnect().finally(() => {
65
+ this.connectPromise = null;
66
+ });
67
+ return this.connectPromise;
68
+ }
69
+ /** Close the WebSocket and release resources. */
70
+ async close() {
71
+ if (this.reconnectTimer) {
72
+ clearTimeout(this.reconnectTimer);
73
+ this.reconnectTimer = null;
74
+ }
75
+ this.setState('closing');
76
+ if (this.socket && this.socket.readyState === READY_STATE_OPEN) {
77
+ this.socket.close(1000, 'client close');
78
+ }
79
+ this.setState('closed');
80
+ for (const pending of this.pending.values()) {
81
+ pending.reject(new Error('connection closed'));
82
+ }
83
+ this.pending.clear();
84
+ }
85
+ /**
86
+ * Send a frame that expects an ack. Returns the matching AckFrame, or
87
+ * rejects with the server's ErrorFrame (wrapped in an Error).
88
+ */
89
+ async request(frame) {
90
+ await this.connect();
91
+ const id = this.nextRequestId++;
92
+ const out = { ...frame, id };
93
+ return new Promise((resolve, reject) => {
94
+ this.pending.set(id, { resolve, reject });
95
+ try {
96
+ this.sendRaw(out);
97
+ }
98
+ catch (err) {
99
+ this.pending.delete(id);
100
+ reject(err instanceof Error ? err : new Error(String(err)));
101
+ }
102
+ });
103
+ }
104
+ /** Send a fire-and-forget frame (no ack expected). */
105
+ async send(frame) {
106
+ await this.connect();
107
+ this.sendRaw(frame);
108
+ }
109
+ /**
110
+ * Register listeners for a channel. Connection remembers the
111
+ * registration so it can re-attach across reconnects, but actually
112
+ * issuing the `sub` frame is the caller's job (Channel does that).
113
+ */
114
+ addChannelListeners(channel) {
115
+ let entry = this.channelListeners.get(channel);
116
+ if (!entry) {
117
+ entry = { messages: new Set(), presence: new Set() };
118
+ this.channelListeners.set(channel, entry);
119
+ }
120
+ return entry;
121
+ }
122
+ /** Forget all listeners for a channel. Called from Channel.detach. */
123
+ removeChannelListeners(channel) {
124
+ this.channelListeners.delete(channel);
125
+ }
126
+ /** Add `channel` to the set of subscriptions to restore on reconnect. */
127
+ rememberSubscription(channel) {
128
+ this.desiredSubscriptions.add(channel);
129
+ }
130
+ /** Stop restoring this subscription on future reconnects. */
131
+ forgetSubscription(channel) {
132
+ this.desiredSubscriptions.delete(channel);
133
+ }
134
+ // ---- internals ----
135
+ async doConnect() {
136
+ this.setState('connecting');
137
+ const ws = this.makeSocket();
138
+ this.socket = ws;
139
+ const authFrame = await this.createAuthFrame();
140
+ return new Promise((resolve, reject) => {
141
+ const onOpen = () => {
142
+ try {
143
+ ws.send(JSON.stringify(authFrame));
144
+ }
145
+ catch (err) {
146
+ reject(err instanceof Error ? err : new Error(String(err)));
147
+ }
148
+ };
149
+ const onAuthMessage = (event) => {
150
+ let parsed;
151
+ try {
152
+ parsed = JSON.parse(typeof event.data === 'string' ? event.data : event.data.toString());
153
+ }
154
+ catch (err) {
155
+ reject(new Error(`failed to parse auth response: ${err.message}`));
156
+ ws.close(1002, 'bad auth response');
157
+ return;
158
+ }
159
+ if (parsed.t === 'connected') {
160
+ const connected = parsed;
161
+ this.connectionId = connected.connectionId;
162
+ this.serverClientId = connected.clientId;
163
+ this.reconnectAttempt = 0;
164
+ // Hand future frames over to the steady-state handler.
165
+ ws.removeEventListener('message', onAuthMessage);
166
+ ws.addEventListener('message', this.handleMessage);
167
+ this.setState('connected');
168
+ resolve();
169
+ this.restoreSubscriptionsOnReconnect();
170
+ }
171
+ else if (parsed.t === 'err') {
172
+ const errFrame = parsed;
173
+ ws.close(1002, `auth error ${errFrame.code}`);
174
+ reject(new Error(`auth failed: ${errFrame.code} ${errFrame.message}`));
175
+ }
176
+ else {
177
+ reject(new Error(`unexpected first frame: ${parsed.t}`));
178
+ ws.close(1002, 'unexpected frame');
179
+ }
180
+ };
181
+ const onError = (event) => {
182
+ const errMessage = event.message ?? 'websocket error';
183
+ reject(new Error(`websocket error: ${errMessage}`));
184
+ };
185
+ const onClose = (event) => {
186
+ this.handleClose(event);
187
+ // If close fires before we finished the handshake, the
188
+ // surrounding promise hasn't been settled yet — surface it as
189
+ // a connect failure.
190
+ reject(new Error(`websocket closed during handshake: ${event.code} ${event.reason}`));
191
+ };
192
+ ws.addEventListener('open', onOpen);
193
+ ws.addEventListener('message', onAuthMessage);
194
+ ws.addEventListener('error', onError);
195
+ ws.addEventListener('close', onClose, { once: true });
196
+ });
197
+ }
198
+ makeSocket() {
199
+ const ctor = this.options.webSocket ?? globalThis.WebSocket;
200
+ if (!ctor) {
201
+ throw new Error('Connection: no WebSocket implementation available. Pass options.webSocket or upgrade to Node 22+.');
202
+ }
203
+ return new ctor(this.options.url);
204
+ }
205
+ async createAuthFrame() {
206
+ if (this.options.key) {
207
+ return {
208
+ t: 'auth',
209
+ key: this.options.key,
210
+ ...(this.options.clientId ? { clientId: this.options.clientId } : {}),
211
+ };
212
+ }
213
+ if (this.options.token)
214
+ return { t: 'auth', token: this.options.token };
215
+ if (!this.options.authCallback) {
216
+ throw new Error('Connection: missing auth method');
217
+ }
218
+ return { t: 'auth', token: await this.options.authCallback() };
219
+ }
220
+ /** Steady-state message handler; installed after a successful auth. */
221
+ handleMessage = (event) => {
222
+ let frame;
223
+ try {
224
+ frame = JSON.parse(typeof event.data === 'string' ? event.data : event.data.toString());
225
+ }
226
+ catch {
227
+ return;
228
+ }
229
+ switch (frame.t) {
230
+ case 'ack': {
231
+ const pending = this.pending.get(frame.id);
232
+ if (pending) {
233
+ this.pending.delete(frame.id);
234
+ pending.resolve(frame);
235
+ }
236
+ return;
237
+ }
238
+ case 'err': {
239
+ if (frame.id != null) {
240
+ const pending = this.pending.get(frame.id);
241
+ if (pending) {
242
+ this.pending.delete(frame.id);
243
+ pending.reject(new Error(`server error ${frame.code}: ${frame.message}`));
244
+ return;
245
+ }
246
+ }
247
+ // Unscoped errors (id 0 or missing) are surfaced via state-change
248
+ // listeners; SDK consumers can subscribe via onStateChange.
249
+ for (const listener of this.stateListeners) {
250
+ listener(this.state, new Error(`server error ${frame.code}: ${frame.message}`));
251
+ }
252
+ return;
253
+ }
254
+ case 'msg': {
255
+ const listeners = this.channelListeners.get(frame.channel);
256
+ if (listeners) {
257
+ for (const listener of listeners.messages)
258
+ listener(frame);
259
+ }
260
+ return;
261
+ }
262
+ case 'presEvt': {
263
+ const listeners = this.channelListeners.get(frame.channel);
264
+ if (listeners) {
265
+ for (const listener of listeners.presence)
266
+ listener(frame);
267
+ }
268
+ return;
269
+ }
270
+ case 'pong':
271
+ case 'connected':
272
+ case 'histRes':
273
+ // Connected can only fire once (we removed the auth listener
274
+ // above); pong and histRes are unused in the MVP — silent
275
+ // forwarding keeps the switch exhaustive.
276
+ return;
277
+ }
278
+ };
279
+ handleClose(_event) {
280
+ this.socket = null;
281
+ if (this.state === 'closing' || this.state === 'closed') {
282
+ this.setState('closed');
283
+ return;
284
+ }
285
+ this.setState('disconnected');
286
+ if (this.options.autoReconnect === false)
287
+ return;
288
+ this.scheduleReconnect();
289
+ }
290
+ scheduleReconnect() {
291
+ if (this.reconnectTimer)
292
+ return;
293
+ const initial = this.options.initialReconnectDelayMs ?? DEFAULT_INITIAL_RECONNECT_DELAY_MS;
294
+ const max = this.options.maxReconnectDelayMs ?? DEFAULT_MAX_RECONNECT_DELAY_MS;
295
+ const delay = Math.min(initial * 2 ** this.reconnectAttempt, max);
296
+ this.reconnectAttempt += 1;
297
+ this.reconnectTimer = setTimeout(() => {
298
+ this.reconnectTimer = null;
299
+ this.connect().catch(() => {
300
+ // doConnect itself drove the state machine; schedule another
301
+ // attempt unless we've been explicitly closed in the meantime.
302
+ if (this.state !== 'closed' && this.state !== 'closing') {
303
+ this.scheduleReconnect();
304
+ }
305
+ });
306
+ }, delay);
307
+ }
308
+ restoreSubscriptionsOnReconnect() {
309
+ // The Channel layer is responsible for re-issuing `sub` frames; we
310
+ // expose desiredSubscriptions so it can iterate without leaking the
311
+ // set.
312
+ for (const channel of this.desiredSubscriptions) {
313
+ this.request({ t: 'sub', channel }).catch(() => {
314
+ // Failure to restore a subscription bubbles up via state
315
+ // listeners on the next request; nothing else to do here.
316
+ });
317
+ }
318
+ }
319
+ sendRaw(frame) {
320
+ if (!this.socket || this.socket.readyState !== READY_STATE_OPEN) {
321
+ throw new Error(`Connection.sendRaw: socket not open (state=${this.state})`);
322
+ }
323
+ this.socket.send(JSON.stringify(frame));
324
+ }
325
+ setState(state) {
326
+ if (this.state === state)
327
+ return;
328
+ this.state = state;
329
+ for (const listener of this.stateListeners)
330
+ listener(state);
331
+ }
332
+ }
333
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"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"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Public entry point for @foony/realtime.
3
+ *
4
+ * Public surface: a `Realtime` class, a `channels.get(name)` registry,
5
+ * and per-channel `subscribe` / `publish` / `presence` methods.
6
+ *
7
+ * For server-side token minting see `@foony/realtime/server`.
8
+ */
9
+ export { Realtime, type RealtimeOptions } from './realtime.js';
10
+ export { Channel, Presence, type UnsubscribeFn } from './channel.js';
11
+ export { Connection, type ConnectionOptions, type ConnectionState, type ConnectionStateListener, type MessageListener, type PresenceEventListener, } from './connection.js';
12
+ export { type AckFrame, type AuthFrame, type ClientFrame, type ConnectedFrame, type ErrorFrame, type FrameType, type HistoryResponseFrame, type MessageFrame, type PingFrame, type PongFrame, type PresenceAction, type PresenceEventFrame, type PresenceFrame, type PublishFrame, type ServerFrame, type SubscribeFrame, type UnsubscribeFrame, ErrorCode, type ErrorCodeName, } from './wire.js';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EACL,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,KAAK,qBAAqB,GAC3B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,SAAS,EACT,KAAK,aAAa,GACnB,MAAM,WAAW,CAAC"}
package/lib/index.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Public entry point for @foony/realtime.
3
+ *
4
+ * Public surface: a `Realtime` class, a `channels.get(name)` registry,
5
+ * and per-channel `subscribe` / `publish` / `presence` methods.
6
+ *
7
+ * For server-side token minting see `@foony/realtime/server`.
8
+ */
9
+ export { Realtime } from './realtime.js';
10
+ export { Channel, Presence } from './channel.js';
11
+ export { Connection, } from './connection.js';
12
+ export { ErrorCode, } from './wire.js';
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,QAAQ,EAAwB,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAsB,MAAM,cAAc,CAAC;AACrE,OAAO,EACL,UAAU,GAMX,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAkBL,SAAS,GAEV,MAAM,WAAW,CAAC"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Realtime is the top-level client class. It owns a Connection and a
3
+ * `channels.get(name)` registry — the public entry point for app code.
4
+ */
5
+ import { Channel } from './channel.js';
6
+ import { type ConnectionOptions, type ConnectionState, type ConnectionStateListener } from './connection.js';
7
+ /** Options for the Realtime client; mirrors ConnectionOptions. */
8
+ export type RealtimeOptions = ConnectionOptions;
9
+ /**
10
+ * Realtime client — call `new Realtime({ url, token })` and use
11
+ * `client.channels.get('chat:1')` to start sending and receiving.
12
+ */
13
+ export declare class Realtime {
14
+ private readonly connection;
15
+ private readonly channelsByName;
16
+ /** Map-like accessor for channels. Stable instance per name. */
17
+ readonly channels: {
18
+ get: (name: string) => Channel;
19
+ release: (name: string) => void;
20
+ };
21
+ constructor(options: RealtimeOptions);
22
+ /** Eagerly open the WebSocket. Optional — channels attach lazily. */
23
+ connect(): Promise<void>;
24
+ /** Close the WebSocket and release every channel. */
25
+ close(): Promise<void>;
26
+ /** Current connection state. */
27
+ getState(): ConnectionState;
28
+ /** Server-issued connection id, populated after auth. */
29
+ getConnectionId(): string | null;
30
+ /** Server-confirmed client id (from the JWT), populated after auth. */
31
+ getClientId(): string | null;
32
+ /** Register a connection-state listener. Returns an unsubscribe fn. */
33
+ onStateChange(listener: ConnectionStateListener): () => void;
34
+ }
35
+ //# sourceMappingURL=realtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime.d.ts","sourceRoot":"","sources":["../src/realtime.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC7B,MAAM,iBAAiB,CAAC;AAEzB,kEAAkE;AAClE,MAAM,MAAM,eAAe,GAAG,iBAAiB,CAAC;AAEhD;;;GAGG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IACxC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA8B;IAE7D,gEAAgE;IAChE,QAAQ,CAAC,QAAQ;oBACH,MAAM,KAAG,OAAO;wBAQZ,MAAM,KAAG,IAAI;MAO7B;gBAEU,OAAO,EAAE,eAAe;IAIpC,qEAAqE;IAC/D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAI9B,qDAAqD;IAC/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B,gCAAgC;IAChC,QAAQ,IAAI,eAAe;IAI3B,yDAAyD;IACzD,eAAe,IAAI,MAAM,GAAG,IAAI;IAIhC,uEAAuE;IACvE,WAAW,IAAI,MAAM,GAAG,IAAI;IAI5B,uEAAuE;IACvE,aAAa,CAAC,QAAQ,EAAE,uBAAuB,GAAG,MAAM,IAAI;CAG7D"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Realtime is the top-level client class. It owns a Connection and a
3
+ * `channels.get(name)` registry — the public entry point for app code.
4
+ */
5
+ import { Channel } from './channel.js';
6
+ import { Connection, } from './connection.js';
7
+ /**
8
+ * Realtime client — call `new Realtime({ url, token })` and use
9
+ * `client.channels.get('chat:1')` to start sending and receiving.
10
+ */
11
+ export class Realtime {
12
+ connection;
13
+ channelsByName = new Map();
14
+ /** Map-like accessor for channels. Stable instance per name. */
15
+ channels = {
16
+ get: (name) => {
17
+ let existing = this.channelsByName.get(name);
18
+ if (!existing) {
19
+ existing = new Channel(this.connection, name);
20
+ this.channelsByName.set(name, existing);
21
+ }
22
+ return existing;
23
+ },
24
+ release: (name) => {
25
+ const channel = this.channelsByName.get(name);
26
+ if (!channel)
27
+ return;
28
+ this.channelsByName.delete(name);
29
+ this.connection.removeChannelListeners(name);
30
+ channel.detach().catch(() => { });
31
+ },
32
+ };
33
+ constructor(options) {
34
+ this.connection = new Connection(options);
35
+ }
36
+ /** Eagerly open the WebSocket. Optional — channels attach lazily. */
37
+ async connect() {
38
+ await this.connection.connect();
39
+ }
40
+ /** Close the WebSocket and release every channel. */
41
+ async close() {
42
+ for (const name of [...this.channelsByName.keys()]) {
43
+ this.channels.release(name);
44
+ }
45
+ await this.connection.close();
46
+ }
47
+ /** Current connection state. */
48
+ getState() {
49
+ return this.connection.getState();
50
+ }
51
+ /** Server-issued connection id, populated after auth. */
52
+ getConnectionId() {
53
+ return this.connection.getConnectionId();
54
+ }
55
+ /** Server-confirmed client id (from the JWT), populated after auth. */
56
+ getClientId() {
57
+ return this.connection.getClientId();
58
+ }
59
+ /** Register a connection-state listener. Returns an unsubscribe fn. */
60
+ onStateChange(listener) {
61
+ return this.connection.onStateChange(listener);
62
+ }
63
+ }
64
+ //# sourceMappingURL=realtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime.js","sourceRoot":"","sources":["../src/realtime.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EACL,UAAU,GAIX,MAAM,iBAAiB,CAAC;AAKzB;;;GAGG;AACH,MAAM,OAAO,QAAQ;IACF,UAAU,CAAa;IACvB,cAAc,GAAG,IAAI,GAAG,EAAmB,CAAC;IAE7D,gEAAgE;IACvD,QAAQ,GAAG;QAClB,GAAG,EAAE,CAAC,IAAY,EAAW,EAAE;YAC7B,IAAI,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC7C,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,QAAQ,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;gBAC9C,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YAC1C,CAAC;YACD,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,OAAO,EAAE,CAAC,IAAY,EAAQ,EAAE;YAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9C,IAAI,CAAC,OAAO;gBAAE,OAAO;YACrB,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACjC,IAAI,CAAC,UAAU,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC;YAC7C,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACnC,CAAC;KACF,CAAC;IAEF,YAAY,OAAwB;QAClC,IAAI,CAAC,UAAU,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;IAC5C,CAAC;IAED,qEAAqE;IACrE,KAAK,CAAC,OAAO;QACX,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;IAClC,CAAC;IAED,qDAAqD;IACrD,KAAK,CAAC,KAAK;QACT,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YACnD,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC9B,CAAC;QACD,MAAM,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;IAChC,CAAC;IAED,gCAAgC;IAChC,QAAQ;QACN,OAAO,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;IACpC,CAAC;IAED,yDAAyD;IACzD,eAAe;QACb,OAAO,IAAI,CAAC,UAAU,CAAC,eAAe,EAAE,CAAC;IAC3C,CAAC;IAED,uEAAuE;IACvE,WAAW;QACT,OAAO,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC;IACvC,CAAC;IAED,uEAAuE;IACvE,aAAa,CAAC,QAAiC;QAC7C,OAAO,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IACjD,CAAC;CACF"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Server-side helpers for the Foony Realtime SDK.
3
+ *
4
+ * `mintRealtimeToken` produces the same HS256 JWT layout the Go edge
5
+ * binary verifies (see `services/realtime-saas/internal/auth/jwt.go`).
6
+ * Foony's application server uses this to hand short-lived tokens to
7
+ * its own clients — the client passes the result into the SDK's
8
+ * `authCallback`.
9
+ *
10
+ * Node-only: relies on `node:crypto` for HMAC-SHA256. Browsers should
11
+ * never see this module.
12
+ */
13
+ /** Options to mint a Foony Realtime token. */
14
+ export type MintRealtimeTokenOptions = {
15
+ /**
16
+ * The HS256 signing key — must exactly match the edge binary's
17
+ * `JWT_SIGNING_KEY` env var. 32 bytes recommended for production.
18
+ */
19
+ readonly signingKey: string | Uint8Array;
20
+ /** App id the token is scoped to. Encoded in the JWT `app` claim. */
21
+ readonly appId: string;
22
+ /** Customer-controlled identifier for the end user (the JWT `sub`). */
23
+ readonly clientId: string;
24
+ /**
25
+ * Capability JSON string scoping the token (e.g. `{"chat:*":["subscribe","publish"]}`).
26
+ * Defaults to `{"*":["*"]}` (full wildcard). The MVP edge does not
27
+ * enforce capabilities yet, but tokens minted today will be checked
28
+ * once enforcement lands.
29
+ */
30
+ readonly capability?: string;
31
+ /**
32
+ * Token TTL. Defaults to 1 hour. Must be > 0; long-lived tokens are
33
+ * an antipattern — the SDK calls `authCallback` on every reconnect
34
+ * so 5-15 minutes is usually plenty.
35
+ */
36
+ readonly ttlMs?: number;
37
+ };
38
+ /**
39
+ * Returns a compact-encoded HS256 JWT carrying the supplied claims.
40
+ *
41
+ * The token shape matches what `services/realtime-saas/internal/auth`
42
+ * mints with `MintForDev` plus a configurable capability and TTL.
43
+ */
44
+ export declare function mintRealtimeToken(options: MintRealtimeTokenOptions): string;
45
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,8CAA8C;AAC9C,MAAM,MAAM,wBAAwB,GAAG;IACrC;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,UAAU,CAAC;IACzC,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAKF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,MAAM,CAmB3E"}
package/lib/server.js ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Server-side helpers for the Foony Realtime SDK.
3
+ *
4
+ * `mintRealtimeToken` produces the same HS256 JWT layout the Go edge
5
+ * binary verifies (see `services/realtime-saas/internal/auth/jwt.go`).
6
+ * Foony's application server uses this to hand short-lived tokens to
7
+ * its own clients — the client passes the result into the SDK's
8
+ * `authCallback`.
9
+ *
10
+ * Node-only: relies on `node:crypto` for HMAC-SHA256. Browsers should
11
+ * never see this module.
12
+ */
13
+ import { createHmac } from 'node:crypto';
14
+ const DEFAULT_TTL_MS = 60 * 60 * 1_000;
15
+ const DEFAULT_CAPABILITY = '{"*":["*"]}';
16
+ /**
17
+ * Returns a compact-encoded HS256 JWT carrying the supplied claims.
18
+ *
19
+ * The token shape matches what `services/realtime-saas/internal/auth`
20
+ * mints with `MintForDev` plus a configurable capability and TTL.
21
+ */
22
+ export function mintRealtimeToken(options) {
23
+ const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
24
+ if (ttlMs <= 0) {
25
+ throw new Error('mintRealtimeToken: ttlMs must be > 0');
26
+ }
27
+ const issuedAtSec = Math.floor(Date.now() / 1_000);
28
+ const expiresAtSec = issuedAtSec + Math.floor(ttlMs / 1_000);
29
+ const payload = {
30
+ sub: options.clientId,
31
+ app: options.appId,
32
+ cap: options.capability ?? DEFAULT_CAPABILITY,
33
+ iat: issuedAtSec,
34
+ exp: expiresAtSec,
35
+ };
36
+ const header = { alg: 'HS256', typ: 'JWT' };
37
+ const signingInput = `${encodeSegment(header)}.${encodeSegment(payload)}`;
38
+ const key = typeof options.signingKey === 'string' ? Buffer.from(options.signingKey, 'utf8') : Buffer.from(options.signingKey);
39
+ const signature = createHmac('sha256', key).update(signingInput).digest();
40
+ return `${signingInput}.${base64UrlEncode(signature)}`;
41
+ }
42
+ /**
43
+ * Encode a JSON value as a base64url segment. JWT spec requires no
44
+ * padding and the URL-safe alphabet — `Buffer.toString('base64url')`
45
+ * is exactly that, available in Node 16+.
46
+ */
47
+ function encodeSegment(value) {
48
+ return base64UrlEncode(Buffer.from(JSON.stringify(value), 'utf8'));
49
+ }
50
+ function base64UrlEncode(buffer) {
51
+ return buffer.toString('base64url');
52
+ }
53
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA4BzC,MAAM,cAAc,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC;AACvC,MAAM,kBAAkB,GAAG,aAAa,CAAC;AAEzC;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAiC;IACjE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,cAAc,CAAC;IAC9C,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IACD,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC;IACnD,MAAM,YAAY,GAAG,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC;IAC7D,MAAM,OAAO,GAAG;QACd,GAAG,EAAE,OAAO,CAAC,QAAQ;QACrB,GAAG,EAAE,OAAO,CAAC,KAAK;QAClB,GAAG,EAAE,OAAO,CAAC,UAAU,IAAI,kBAAkB;QAC7C,GAAG,EAAE,WAAW;QAChB,GAAG,EAAE,YAAY;KACgB,CAAC;IACpC,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAoC,CAAC;IAC9E,MAAM,YAAY,GAAG,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC;IAC1E,MAAM,GAAG,GAAG,OAAO,OAAO,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/H,MAAM,SAAS,GAAG,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,EAAE,CAAC;IAC1E,OAAO,GAAG,YAAY,IAAI,eAAe,CAAC,SAAS,CAAC,EAAE,CAAC;AACzD,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACrE,CAAC;AAED,SAAS,eAAe,CAAC,MAAc;IACrC,OAAO,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACtC,CAAC"}