webspresso 0.0.89 → 0.0.91

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 (102) hide show
  1. package/README.md +450 -53
  2. package/bin/commands/service.js +193 -0
  3. package/bin/webspresso.js +2 -0
  4. package/core/auth/manager.js +36 -26
  5. package/core/auth/middleware.js +21 -2
  6. package/core/compression/compressible.js +51 -0
  7. package/core/compression/compression-stream.js +59 -0
  8. package/core/compression/index.js +25 -0
  9. package/core/compression/middleware.js +301 -0
  10. package/core/compression/negotiate.js +153 -0
  11. package/core/errors/base.js +31 -0
  12. package/core/errors/domain.js +193 -0
  13. package/core/errors/http.js +133 -0
  14. package/core/errors/index.js +77 -0
  15. package/core/errors/middleware.js +162 -0
  16. package/core/errors/normalize.js +171 -0
  17. package/core/orm/complexity.js +120 -0
  18. package/core/orm/events.js +64 -14
  19. package/core/orm/index.js +45 -10
  20. package/core/orm/model.js +10 -2
  21. package/core/orm/query-builder.js +30 -4
  22. package/core/orm/repository.js +57 -25
  23. package/core/orm/scopes.js +2 -1
  24. package/core/orm/transaction.js +39 -5
  25. package/core/orm/utils.js +6 -3
  26. package/core/queue/adapters/database.js +284 -0
  27. package/core/queue/adapters/memory.js +155 -0
  28. package/core/queue/adapters/redis.js +336 -0
  29. package/core/queue/index.js +21 -0
  30. package/core/queue/job.js +103 -0
  31. package/core/queue/manager.js +387 -0
  32. package/core/realtime/adapters/redis.js +431 -0
  33. package/core/realtime/adapters/socket-io.js +203 -0
  34. package/core/realtime/adapters/sse.js +210 -0
  35. package/core/realtime/adapters/websocket.js +337 -0
  36. package/core/realtime/auth-manager.js +140 -0
  37. package/core/realtime/core.js +359 -0
  38. package/core/realtime/emitter.js +98 -0
  39. package/core/realtime/errors.js +71 -0
  40. package/core/realtime/index.js +79 -0
  41. package/core/realtime/reconnect-manager.js +118 -0
  42. package/core/realtime/registry.js +243 -0
  43. package/core/shutdown/http-adapter.js +134 -0
  44. package/core/shutdown/index.js +12 -0
  45. package/core/shutdown/shutdown-manager.js +312 -0
  46. package/core/ssr/index.js +13 -0
  47. package/core/ssr/stream.js +174 -0
  48. package/index.d.ts +829 -52
  49. package/index.js +103 -0
  50. package/package.json +22 -2
  51. package/plugins/admin-panel/api.js +37 -4
  52. package/plugins/admin-panel/app.js +67 -17
  53. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +4 -1
  54. package/plugins/admin-panel/client/parts/03-pagination-intro.js +25 -18
  55. package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
  56. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +37 -23
  57. package/plugins/admin-panel/client/parts/08-record-list.js +27 -34
  58. package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
  59. package/plugins/admin-panel/core/admin-module.js +2 -2
  60. package/plugins/admin-panel/core/api-extensions.js +149 -0
  61. package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
  62. package/plugins/admin-panel/index.js +131 -18
  63. package/plugins/admin-panel/modules/admin-users.js +770 -0
  64. package/plugins/admin-panel/modules/custom-pages.js +144 -45
  65. package/plugins/admin-panel/modules/menu.js +85 -0
  66. package/plugins/admin-panel/modules/user-management.js +5 -2
  67. package/plugins/admin-panel/vendor/mithril.min.js +1 -0
  68. package/plugins/admin-panel/vendor/quill.min.js +7 -0
  69. package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
  70. package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
  71. package/plugins/basic-auth/index.js +293 -0
  72. package/plugins/content/client/inline-edit.js +41 -17
  73. package/plugins/cors.js +12 -8
  74. package/plugins/csrf/index.js +8 -8
  75. package/plugins/data-exchange/export-xlsx.js +5 -1
  76. package/plugins/data-exchange/import.js +16 -2
  77. package/plugins/data-exchange/index.js +14 -0
  78. package/plugins/email/index.js +14 -1
  79. package/plugins/index.js +29 -0
  80. package/plugins/queue/index.js +108 -0
  81. package/plugins/realtime/index.js +150 -0
  82. package/plugins/recaptcha/index.js +11 -0
  83. package/plugins/redirect/index.js +9 -1
  84. package/plugins/rest-resources/index.js +280 -11
  85. package/plugins/upload/index.js +16 -0
  86. package/src/app-context.js +46 -6
  87. package/src/file-router.js +220 -28
  88. package/src/helpers.js +24 -5
  89. package/src/plugin-manager.js +134 -7
  90. package/src/server.js +470 -44
  91. package/src/services/builtins/auth.js +273 -0
  92. package/src/services/builtins/exchange.js +229 -0
  93. package/src/services/builtins/mail.js +134 -0
  94. package/src/services/builtins/media.js +196 -0
  95. package/src/services/builtins/system.js +221 -0
  96. package/src/services/discovery.js +140 -0
  97. package/src/services/executor.js +292 -0
  98. package/src/services/index.js +70 -0
  99. package/src/services/memoize.js +256 -0
  100. package/src/services/registry.js +288 -0
  101. package/src/services/validator.js +281 -0
  102. package/templates/skills/webspresso-usage/REFERENCE-framework.md +2 -2
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Server-Sent Events (SSE) Adapter for Webspresso Realtime
3
+ * @module core/realtime/adapters/sse
4
+ */
5
+
6
+ /**
7
+ * Resolve target SSE URL with auth query parameters
8
+ *
9
+ * @param {string|Function} urlOption
10
+ * @param {Object} context
11
+ * @param {string} [queryParamName='token']
12
+ * @returns {Promise<string>}
13
+ */
14
+ async function resolveSseUrl(urlOption, context, queryParamName = 'token') {
15
+ let resolved = '';
16
+ if (typeof urlOption === 'function') {
17
+ resolved = await urlOption(context);
18
+ } else if (typeof urlOption === 'string') {
19
+ resolved = urlOption;
20
+ } else {
21
+ throw new Error('SSE adapter requires a valid url string or async resolver function');
22
+ }
23
+
24
+ if (context.auth && context.auth.token) {
25
+ const separator = resolved.includes('?') ? '&' : '?';
26
+ const encodedToken = encodeURIComponent(context.auth.token);
27
+ resolved = `${resolved}${separator}${queryParamName}=${encodedToken}`;
28
+ }
29
+
30
+ return resolved;
31
+ }
32
+
33
+ class SseAdapter {
34
+ /**
35
+ * @param {Object} options
36
+ * @param {string|Function} options.url - SSE endpoint URL or async resolver
37
+ * @param {string} [options.queryParamName='token'] - Query parameter name for token
38
+ * @param {Function} [options.EventSource] - EventSource constructor injection
39
+ * @param {Object} [options.capabilities] - Custom capabilities override
40
+ */
41
+ constructor(options = {}) {
42
+ this.options = options;
43
+ this.capabilities = {
44
+ send: false,
45
+ perform: false,
46
+ multiplexing: true,
47
+ serverEvents: true,
48
+ binary: false,
49
+ ...(options.capabilities || {}),
50
+ };
51
+
52
+ this.EventSourceClass = options.EventSource || (typeof EventSource !== 'undefined' ? EventSource : null);
53
+ this.eventSource = null;
54
+ this._listeners = new Map();
55
+ this._subscriptions = new Map();
56
+ this._isOpen = false;
57
+ }
58
+
59
+ isConnected() {
60
+ return Boolean(this.eventSource && this._isOpen);
61
+ }
62
+
63
+ on(event, handler) {
64
+ if (!this._listeners.has(event)) {
65
+ this._listeners.set(event, new Set());
66
+ }
67
+ this._listeners.get(event).add(handler);
68
+ return () => {
69
+ const set = this._listeners.get(event);
70
+ if (set) set.delete(handler);
71
+ };
72
+ }
73
+
74
+ _emit(event, ...args) {
75
+ const set = this._listeners.get(event);
76
+ if (!set) return;
77
+ for (const handler of set) {
78
+ try {
79
+ handler(...args);
80
+ } catch (err) {
81
+ console.error(`[sse-adapter] Error in event listener for "${event}":`, err);
82
+ }
83
+ }
84
+ }
85
+
86
+ async connect(context) {
87
+ if (!this.EventSourceClass) {
88
+ throw new Error('EventSource implementation not found. Inject options.EventSource or run in a browser environment.');
89
+ }
90
+
91
+ const queryParamName = this.options.queryParamName || 'token';
92
+ const url = await resolveSseUrl(this.options.url, context, queryParamName);
93
+
94
+ return new Promise((resolve, reject) => {
95
+ let isSettled = false;
96
+
97
+ try {
98
+ this.eventSource = new this.EventSourceClass(url);
99
+ } catch (err) {
100
+ return reject(err);
101
+ }
102
+
103
+ this.eventSource.onopen = () => {
104
+ this._isOpen = true;
105
+ if (!isSettled) {
106
+ isSettled = true;
107
+ resolve();
108
+ }
109
+ };
110
+
111
+ this.eventSource.onerror = (err) => {
112
+ this._isOpen = false;
113
+ this._emit('disconnected', err);
114
+ if (!isSettled) {
115
+ isSettled = true;
116
+ reject(err || new Error('SSE connection failed'));
117
+ }
118
+ };
119
+
120
+ this.eventSource.onmessage = (event) => {
121
+ this._handleMessage(event.data);
122
+ };
123
+ });
124
+ }
125
+
126
+ _handleMessage(rawData) {
127
+ let data = rawData;
128
+ try {
129
+ data = JSON.parse(rawData);
130
+ } catch {
131
+ // Raw string
132
+ }
133
+
134
+ const identifier = data && typeof data === 'object' ? data.identifier : null;
135
+
136
+ if (identifier && this._subscriptions.has(identifier)) {
137
+ const subs = this._subscriptions.get(identifier);
138
+ for (const cb of subs) {
139
+ cb.received?.(data.data !== undefined ? data.data : data);
140
+ }
141
+ return;
142
+ }
143
+
144
+ for (const subs of this._subscriptions.values()) {
145
+ for (const cb of subs) {
146
+ cb.received?.(data);
147
+ }
148
+ }
149
+ }
150
+
151
+ subscribe(identifier, params, callbacks) {
152
+ if (!this._subscriptions.has(identifier)) {
153
+ this._subscriptions.set(identifier, new Set());
154
+ }
155
+
156
+ this._subscriptions.get(identifier).add(callbacks);
157
+ callbacks.connected?.();
158
+
159
+ return {
160
+ send: () => {
161
+ throw new Error('SSE adapter is unidirectional and does not support send()');
162
+ },
163
+ perform: () => {
164
+ throw new Error('SSE adapter is unidirectional and does not support perform()');
165
+ },
166
+ unsubscribe: () => this.unsubscribe(identifier, params, callbacks),
167
+ };
168
+ }
169
+
170
+ unsubscribe(identifier, params, callbacks) {
171
+ if (this._subscriptions.has(identifier)) {
172
+ const subs = this._subscriptions.get(identifier);
173
+ if (callbacks) {
174
+ subs.delete(callbacks);
175
+ }
176
+ if (subs.size === 0 || !callbacks) {
177
+ this._subscriptions.delete(identifier);
178
+ }
179
+ }
180
+ }
181
+
182
+ disconnect() {
183
+ this._isOpen = false;
184
+ if (this.eventSource) {
185
+ try {
186
+ this.eventSource.close();
187
+ } catch (err) {
188
+ // Closed
189
+ }
190
+ this.eventSource = null;
191
+ }
192
+ }
193
+
194
+ destroy() {
195
+ this.disconnect();
196
+ this._subscriptions.clear();
197
+ this._listeners.clear();
198
+ }
199
+ }
200
+
201
+ function createSseAdapter(options = {}) {
202
+ return new SseAdapter(options);
203
+ }
204
+
205
+ module.exports = {
206
+ createSseAdapter,
207
+ sse: createSseAdapter,
208
+ SseAdapter,
209
+ resolveSseUrl,
210
+ };
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Generic WebSocket Adapter for Webspresso Realtime
3
+ * @module core/realtime/adapters/websocket
4
+ */
5
+
6
+ /**
7
+ * Resolve target WebSocket URL with optional auth query parameters
8
+ *
9
+ * @param {string|Function} urlOption
10
+ * @param {Object} context
11
+ * @param {string} [authTransport='query']
12
+ * @param {string} [queryParamName='token']
13
+ * @returns {Promise<string>}
14
+ */
15
+ async function resolveWebSocketUrl(urlOption, context, authTransport = 'query', queryParamName = 'token') {
16
+ let resolved = '';
17
+ if (typeof urlOption === 'function') {
18
+ resolved = await urlOption(context);
19
+ } else if (typeof urlOption === 'string') {
20
+ resolved = urlOption;
21
+ } else {
22
+ throw new Error('WebSocket adapter requires a valid url string or async resolver function');
23
+ }
24
+
25
+ // Handle query parameter auth transport if token exists
26
+ if (authTransport === 'query' && context.auth && context.auth.token) {
27
+ const separator = resolved.includes('?') ? '&' : '?';
28
+ const encodedToken = encodeURIComponent(context.auth.token);
29
+ resolved = `${resolved}${separator}${queryParamName}=${encodedToken}`;
30
+ }
31
+
32
+ return resolved;
33
+ }
34
+
35
+ /**
36
+ * Default WebSocket message formatters
37
+ */
38
+ const DEFAULT_MESSAGE_FORMATTERS = {
39
+ subscribe: ({ identifier, params }) => ({
40
+ type: 'subscribe',
41
+ identifier,
42
+ params,
43
+ }),
44
+ unsubscribe: ({ identifier, params }) => ({
45
+ type: 'unsubscribe',
46
+ identifier,
47
+ params,
48
+ }),
49
+ perform: ({ identifier, action, data }) => ({
50
+ type: 'perform',
51
+ identifier,
52
+ action,
53
+ data,
54
+ }),
55
+ send: ({ identifier, data }) => ({
56
+ type: 'message',
57
+ identifier,
58
+ data,
59
+ }),
60
+ auth: ({ token, metadata }) => ({
61
+ type: 'auth',
62
+ token,
63
+ ...metadata,
64
+ }),
65
+ };
66
+
67
+ class WebSocketAdapter {
68
+ /**
69
+ * @param {Object} options
70
+ * @param {string|Function} options.url - WebSocket URL or async function
71
+ * @param {string} [options.authTransport='query'] - 'query' | 'header' | 'protocol' | 'message'
72
+ * @param {string} [options.queryParamName='token'] - Query parameter name for token
73
+ * @param {string|string[]} [options.protocols] - Subprotocols
74
+ * @param {Function} [options.WebSocket] - WebSocket constructor injection
75
+ * @param {Object} [options.messages] - Message formatters
76
+ * @param {Object} [options.capabilities] - Custom capabilities
77
+ */
78
+ constructor(options = {}) {
79
+ this.options = options;
80
+ this.capabilities = {
81
+ send: true,
82
+ perform: true,
83
+ multiplexing: true,
84
+ serverEvents: true,
85
+ binary: true,
86
+ ...(options.capabilities || {}),
87
+ };
88
+
89
+ this.messages = {
90
+ ...DEFAULT_MESSAGE_FORMATTERS,
91
+ ...(options.messages || {}),
92
+ };
93
+
94
+ this.WebSocketClass = options.WebSocket || (typeof WebSocket !== 'undefined' ? WebSocket : null);
95
+ this.socket = null;
96
+ this._listeners = new Map();
97
+ this._activeSubscriptions = new Map();
98
+ this._isOpen = false;
99
+ }
100
+
101
+ isConnected() {
102
+ return Boolean(this.socket && this._isOpen);
103
+ }
104
+
105
+ on(event, handler) {
106
+ if (!this._listeners.has(event)) {
107
+ this._listeners.set(event, new Set());
108
+ }
109
+ this._listeners.get(event).add(handler);
110
+ return () => {
111
+ const set = this._listeners.get(event);
112
+ if (set) set.delete(handler);
113
+ };
114
+ }
115
+
116
+ _emit(event, ...args) {
117
+ const set = this._listeners.get(event);
118
+ if (!set) return;
119
+ for (const handler of set) {
120
+ try {
121
+ handler(...args);
122
+ } catch (err) {
123
+ console.error(`[websocket-adapter] Error in event listener for "${event}":`, err);
124
+ }
125
+ }
126
+ }
127
+
128
+ async connect(context) {
129
+ if (!this.WebSocketClass) {
130
+ throw new Error('WebSocket implementation not found. Inject options.WebSocket or run in a browser environment.');
131
+ }
132
+
133
+ const authTransport = this.options.authTransport || 'query';
134
+ const queryParamName = this.options.queryParamName || 'token';
135
+
136
+ const url = await resolveWebSocketUrl(this.options.url, context, authTransport, queryParamName);
137
+
138
+ let protocols = this.options.protocols;
139
+ if (authTransport === 'protocol' && context.auth && context.auth.token) {
140
+ const authProtocol = `token.${context.auth.token}`;
141
+ protocols = Array.isArray(protocols) ? [...protocols, authProtocol] : [authProtocol];
142
+ }
143
+
144
+ return new Promise((resolve, reject) => {
145
+ let isSettled = false;
146
+
147
+ try {
148
+ this.socket = protocols ? new this.WebSocketClass(url, protocols) : new this.WebSocketClass(url);
149
+ } catch (err) {
150
+ return reject(err);
151
+ }
152
+
153
+ this.socket.onopen = () => {
154
+ this._isOpen = true;
155
+ isSettled = true;
156
+
157
+ if (authTransport === 'message' && context.auth && context.auth.token) {
158
+ try {
159
+ const authMsg = this.messages.auth({
160
+ token: context.auth.token,
161
+ metadata: context.auth,
162
+ });
163
+ this.socket.send(typeof authMsg === 'string' ? authMsg : JSON.stringify(authMsg));
164
+ } catch (e) {
165
+ console.error('[websocket-adapter] Failed to send auth message:', e);
166
+ }
167
+ }
168
+
169
+ resolve();
170
+ };
171
+
172
+ this.socket.onclose = (event) => {
173
+ this._isOpen = false;
174
+ const code = event ? event.code : null;
175
+ const reason = event ? event.reason : '';
176
+
177
+ if (code === 4001 || code === 4003 || code === 4401 || (typeof reason === 'string' && reason.toLowerCase().includes('unauthorized'))) {
178
+ this._emit('unauthorized', new Error(reason || 'Unauthorized'));
179
+ }
180
+
181
+ this._emit('disconnected', { code, reason });
182
+
183
+ if (!isSettled) {
184
+ isSettled = true;
185
+ reject(new Error(`WebSocket connection closed before opening: ${code} ${reason}`));
186
+ }
187
+ };
188
+
189
+ this.socket.onerror = (err) => {
190
+ this._emit('error', err);
191
+ if (!isSettled) {
192
+ isSettled = true;
193
+ reject(err || new Error('WebSocket connection failed'));
194
+ }
195
+ };
196
+
197
+ this.socket.onmessage = (event) => {
198
+ this._handleMessage(event.data);
199
+ };
200
+ });
201
+ }
202
+
203
+ _handleMessage(rawData) {
204
+ let message = rawData;
205
+ if (typeof rawData === 'string') {
206
+ try {
207
+ message = JSON.parse(rawData);
208
+ } catch {
209
+ // Raw string message
210
+ }
211
+ }
212
+
213
+ if (!message || typeof message !== 'object') {
214
+ for (const subs of this._activeSubscriptions.values()) {
215
+ for (const cb of subs.values()) {
216
+ cb.received?.(message);
217
+ }
218
+ }
219
+ return;
220
+ }
221
+
222
+ const { identifier, type, data, error } = message;
223
+
224
+ if (type === 'unauthorized' || type === 'auth_error') {
225
+ this._emit('unauthorized', new Error(message.message || 'Unauthorized'));
226
+ return;
227
+ }
228
+
229
+ if (identifier && this._activeSubscriptions.has(identifier)) {
230
+ const subs = this._activeSubscriptions.get(identifier);
231
+ for (const cb of subs.values()) {
232
+ if (type === 'rejected') {
233
+ cb.rejected?.(error || new Error(message.message || 'Subscription rejected'));
234
+ } else if (type === 'subscribed' || type === 'connected') {
235
+ cb.connected?.();
236
+ } else {
237
+ cb.received?.(data !== undefined ? data : message);
238
+ }
239
+ }
240
+ return;
241
+ }
242
+
243
+ for (const subs of this._activeSubscriptions.values()) {
244
+ for (const cb of subs.values()) {
245
+ cb.received?.(message);
246
+ }
247
+ }
248
+ }
249
+
250
+ subscribe(identifier, params, callbacks) {
251
+ if (!this._activeSubscriptions.has(identifier)) {
252
+ this._activeSubscriptions.set(identifier, new Map());
253
+ }
254
+
255
+ const subId = Symbol('sub');
256
+ this._activeSubscriptions.get(identifier).set(subId, callbacks);
257
+
258
+ if (this.isConnected()) {
259
+ const msg = this.messages.subscribe({ identifier, params });
260
+ this._sendRaw(msg);
261
+ callbacks.connected?.();
262
+ }
263
+
264
+ return {
265
+ send: (data) => this.send(identifier, data),
266
+ perform: (action, data) => this.perform(identifier, action, data),
267
+ unsubscribe: () => this.unsubscribe(identifier, params, subId),
268
+ };
269
+ }
270
+
271
+ unsubscribe(identifier, params, subId) {
272
+ if (this._activeSubscriptions.has(identifier)) {
273
+ const subs = this._activeSubscriptions.get(identifier);
274
+ if (subId) {
275
+ subs.delete(subId);
276
+ }
277
+ if (subs.size === 0 || !subId) {
278
+ this._activeSubscriptions.delete(identifier);
279
+ if (this.isConnected()) {
280
+ const msg = this.messages.unsubscribe({ identifier, params });
281
+ this._sendRaw(msg);
282
+ }
283
+ }
284
+ }
285
+ }
286
+
287
+ send(identifier, data) {
288
+ const msg = this.messages.send({ identifier, data });
289
+ this._sendRaw(msg);
290
+ }
291
+
292
+ perform(identifier, action, data) {
293
+ const msg = this.messages.perform({ identifier, action, data });
294
+ this._sendRaw(msg);
295
+ }
296
+
297
+ _sendRaw(msg) {
298
+ if (!this.isConnected()) {
299
+ throw new Error('WebSocket is not connected');
300
+ }
301
+ const payload = typeof msg === 'string' ? msg : JSON.stringify(msg);
302
+ this.socket.send(payload);
303
+ }
304
+
305
+ disconnect() {
306
+ this._isOpen = false;
307
+ if (this.socket) {
308
+ try {
309
+ this.socket.onopen = null;
310
+ this.socket.onclose = null;
311
+ this.socket.onerror = null;
312
+ this.socket.onmessage = null;
313
+ this.socket.close();
314
+ } catch (err) {
315
+ // Socket close
316
+ }
317
+ this.socket = null;
318
+ }
319
+ }
320
+
321
+ destroy() {
322
+ this.disconnect();
323
+ this._activeSubscriptions.clear();
324
+ this._listeners.clear();
325
+ }
326
+ }
327
+
328
+ function createWebSocketAdapter(options = {}) {
329
+ return new WebSocketAdapter(options);
330
+ }
331
+
332
+ module.exports = {
333
+ createWebSocketAdapter,
334
+ websocket: createWebSocketAdapter,
335
+ WebSocketAdapter,
336
+ resolveWebSocketUrl,
337
+ };
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Realtime Authentication Strategy Manager
3
+ * @module core/realtime/auth-manager
4
+ */
5
+
6
+ const { RealtimeError } = require('./errors');
7
+
8
+ class AuthManager {
9
+ /**
10
+ * @param {Object|boolean} [authConfig=false]
11
+ */
12
+ constructor(authConfig = false) {
13
+ this.config = authConfig;
14
+ }
15
+
16
+ /**
17
+ * Check if authentication is enabled
18
+ * @returns {boolean}
19
+ */
20
+ isEnabled() {
21
+ return Boolean(this.config && this.config !== false);
22
+ }
23
+
24
+ /**
25
+ * Get active auth strategy ('token' | 'cookie' | 'custom' | 'none')
26
+ * @returns {'token' | 'cookie' | 'custom' | 'none'}
27
+ */
28
+ getStrategy() {
29
+ if (!this.isEnabled()) return 'none';
30
+ return this.config.strategy || 'token';
31
+ }
32
+
33
+ /**
34
+ * Resolve framework-agnostic connection auth metadata before establishing connection
35
+ *
36
+ * @returns {Promise<Object>} Metadata to pass to adapter
37
+ */
38
+ async resolveAuthMetadata() {
39
+ if (!this.isEnabled()) {
40
+ return { type: 'none' };
41
+ }
42
+
43
+ const strategy = this.getStrategy();
44
+
45
+ if (strategy === 'cookie') {
46
+ return { type: 'cookie' };
47
+ }
48
+
49
+ if (strategy === 'token') {
50
+ if (typeof this.config.getToken !== 'function') {
51
+ throw new RealtimeError(
52
+ 'UNAUTHORIZED',
53
+ 'Auth strategy "token" requires a getToken() function'
54
+ );
55
+ }
56
+ try {
57
+ const token = await this.config.getToken();
58
+ if (!token) {
59
+ throw new RealtimeError('UNAUTHORIZED', 'No authentication token provided by getToken()');
60
+ }
61
+ return {
62
+ type: 'token',
63
+ token,
64
+ };
65
+ } catch (err) {
66
+ if (err instanceof RealtimeError) throw err;
67
+ throw new RealtimeError('UNAUTHORIZED', 'Failed to retrieve authentication token', err);
68
+ }
69
+ }
70
+
71
+ if (strategy === 'custom') {
72
+ if (typeof this.config.resolve !== 'function') {
73
+ throw new RealtimeError(
74
+ 'UNAUTHORIZED',
75
+ 'Auth strategy "custom" requires a resolve() function'
76
+ );
77
+ }
78
+ try {
79
+ const metadata = await this.config.resolve();
80
+ return {
81
+ type: 'custom',
82
+ ...(metadata && typeof metadata === 'object' ? metadata : {}),
83
+ };
84
+ } catch (err) {
85
+ if (err instanceof RealtimeError) throw err;
86
+ throw new RealtimeError('UNAUTHORIZED', 'Custom auth resolution failed', err);
87
+ }
88
+ }
89
+
90
+ throw new RealtimeError('UNAUTHORIZED', `Unsupported auth strategy "${strategy}"`);
91
+ }
92
+
93
+ /**
94
+ * Handle token refresh or re-authentication
95
+ *
96
+ * @returns {Promise<Object>} New auth metadata
97
+ */
98
+ async refreshToken() {
99
+ if (!this.isEnabled()) {
100
+ return { type: 'none' };
101
+ }
102
+
103
+ const strategy = this.getStrategy();
104
+
105
+ if (strategy === 'token' && typeof this.config.refreshToken === 'function') {
106
+ try {
107
+ const token = await this.config.refreshToken();
108
+ if (!token) {
109
+ throw new RealtimeError('TOKEN_REFRESH_FAILED', 'refreshToken() returned empty token');
110
+ }
111
+ return {
112
+ type: 'token',
113
+ token,
114
+ };
115
+ } catch (err) {
116
+ if (err instanceof RealtimeError) throw err;
117
+ throw new RealtimeError('TOKEN_REFRESH_FAILED', 'Failed to refresh token', err);
118
+ }
119
+ }
120
+
121
+ // Fallback to standard resolve
122
+ return this.resolveAuthMetadata();
123
+ }
124
+
125
+ /**
126
+ * Notify unauthorized handler if configured
127
+ * @param {RealtimeError} error
128
+ */
129
+ handleUnauthorized(error) {
130
+ if (this.config && typeof this.config.onUnauthorized === 'function') {
131
+ try {
132
+ this.config.onUnauthorized(error);
133
+ } catch (e) {
134
+ console.error('[realtime] Error in onUnauthorized handler:', e);
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ module.exports = { AuthManager };