@africode/core 5.0.0 → 5.0.2

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,44 @@
1
+ /**
2
+ * Request identity resolver for AfriCode sessions.
3
+ *
4
+ * @module core/request-identity
5
+ */
6
+
7
+ const SESSION_COOKIE_NAME = 'afri_session';
8
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
9
+
10
+ export function parseCookieHeader(cookieHeader = '') {
11
+ return cookieHeader
12
+ .split(';')
13
+ .map((part) => part.trim())
14
+ .filter(Boolean)
15
+ .reduce((cookies, part) => {
16
+ const separatorIndex = part.indexOf('=');
17
+ if (separatorIndex === -1) {
18
+ return cookies;
19
+ }
20
+
21
+ const key = part.slice(0, separatorIndex).trim();
22
+ const value = part.slice(separatorIndex + 1).trim();
23
+ cookies[key] = value;
24
+ return cookies;
25
+ }, {});
26
+ }
27
+
28
+ export function createSessionId() {
29
+ return crypto.randomUUID();
30
+ }
31
+
32
+ export function getRequestIdentity(req) {
33
+ const cookieHeader = req?.headers?.get?.('cookie') || '';
34
+ const cookies = parseCookieHeader(cookieHeader);
35
+ const sessionId = cookies[SESSION_COOKIE_NAME];
36
+
37
+ if (sessionId && UUID_PATTERN.test(sessionId)) {
38
+ return sessionId;
39
+ }
40
+
41
+ return createSessionId();
42
+ }
43
+
44
+ export { SESSION_COOKIE_NAME };
package/core/sdk.js CHANGED
@@ -13,6 +13,10 @@ import { hydrate } from './hydration.js';
13
13
  import { html, Layout } from './html.js';
14
14
  import { schemas, Validation, rules, afri, AfriFieldBuilder, normalizeInput, buildSchema } from './validation.js';
15
15
  import { createFramework, getConfig } from './config.js';
16
+ import { getRequestIdentity, parseCookieHeader, createSessionId, SESSION_COOKIE_NAME } from './request-identity.js';
17
+ import { sessionStore } from './session-store.js';
18
+ import { actions as runtimeActions } from './actions.js';
19
+ import { MiddlewareManager, loggerMiddleware, authMiddleware } from './middleware.js';
16
20
  import {
17
21
  AfriCodeError, ValidationError, InvalidUrlError,
18
22
  SecurityError, CsrfError, RateLimitError,
@@ -64,6 +68,15 @@ export {
64
68
  buildSchema,
65
69
  createFramework,
66
70
  getConfig,
71
+ getRequestIdentity,
72
+ parseCookieHeader,
73
+ createSessionId,
74
+ SESSION_COOKIE_NAME,
75
+ sessionStore,
76
+ runtimeActions,
77
+ MiddlewareManager,
78
+ loggerMiddleware,
79
+ authMiddleware,
67
80
  AfriCodeError,
68
81
  ValidationError,
69
82
  InvalidUrlError,
@@ -111,6 +124,15 @@ export default {
111
124
  buildSchema,
112
125
  createFramework,
113
126
  getConfig,
127
+ getRequestIdentity,
128
+ parseCookieHeader,
129
+ createSessionId,
130
+ SESSION_COOKIE_NAME,
131
+ sessionStore,
132
+ runtimeActions,
133
+ MiddlewareManager,
134
+ loggerMiddleware,
135
+ authMiddleware,
114
136
  AfriCodeError,
115
137
  ValidationError,
116
138
  InvalidUrlError,
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Server-side session state store for AfriCode.
3
+ *
4
+ * @module core/session-store
5
+ */
6
+
7
+ class SessionStore {
8
+ constructor() {
9
+ this.sessions = new Map();
10
+ }
11
+
12
+ ensure(sessionId) {
13
+ if (!this.sessions.has(sessionId)) {
14
+ this.sessions.set(sessionId, {
15
+ data: {
16
+ counter: { value: 0 }
17
+ },
18
+ meta: {
19
+ createdAt: new Date(),
20
+ lastSeenAt: new Date()
21
+ }
22
+ });
23
+ }
24
+
25
+ return this.sessions.get(sessionId);
26
+ }
27
+
28
+ touch(sessionId) {
29
+ const session = this.ensure(sessionId);
30
+ session.meta.lastSeenAt = new Date();
31
+ return session;
32
+ }
33
+
34
+ has(sessionId) {
35
+ return this.sessions.has(sessionId);
36
+ }
37
+
38
+ get(sessionId) {
39
+ return this.touch(sessionId).data;
40
+ }
41
+
42
+ set(sessionId, path, value) {
43
+ const session = this.get(sessionId);
44
+ const keys = path.split('/');
45
+ let obj = session;
46
+
47
+ while (keys.length > 1) {
48
+ const key = keys.shift();
49
+ if (!obj[key] || typeof obj[key] !== 'object') {
50
+ obj[key] = {};
51
+ }
52
+ obj = obj[key];
53
+ }
54
+
55
+ obj[keys[0]] = value;
56
+ return value;
57
+ }
58
+
59
+ delete(sessionId) {
60
+ return this.sessions.delete(sessionId);
61
+ }
62
+
63
+ clear() {
64
+ this.sessions.clear();
65
+ }
66
+ }
67
+
68
+ export const sessionStore = new SessionStore();
package/core/state.js CHANGED
@@ -124,6 +124,40 @@ function notifySubscribers(state, newValue, oldValue, property) {
124
124
  });
125
125
  }
126
126
 
127
+ /**
128
+ * Simple server-side state store for Phase 3A
129
+ */
130
+ export class GlobalState {
131
+ constructor() {
132
+ this.store = {};
133
+ }
134
+
135
+ get(path) {
136
+ return path.split('/').reduce((obj, key) => obj?.[key], this.store);
137
+ }
138
+
139
+ set(path, value) {
140
+ const keys = path.split('/');
141
+ let obj = this.store;
142
+
143
+ while (keys.length > 1) {
144
+ const key = keys.shift();
145
+ if (!obj[key] || typeof obj[key] !== 'object') {
146
+ obj[key] = {};
147
+ }
148
+ obj = obj[key];
149
+ }
150
+
151
+ obj[keys[0]] = value;
152
+ }
153
+
154
+ getAll() {
155
+ return this.store;
156
+ }
157
+ }
158
+
159
+ export const globalState = new GlobalState();
160
+
127
161
  /**
128
162
  * Create a computed value that updates when dependencies change
129
163
  *
package/core/websocket.js CHANGED
@@ -10,6 +10,8 @@
10
10
  * - Automatic reconnection
11
11
  */
12
12
 
13
+ import { frameworkLog } from './logging.js';
14
+
13
15
  /**
14
16
  * WebSocket Server Manager
15
17
  * Handles connections, rooms, and broadcasting
@@ -117,11 +119,11 @@ export class WebSocketServer {
117
119
  timestamp: Date.now()
118
120
  });
119
121
  } catch (error) {
120
- console.error(`Error in handler for ${type}:`, error);
122
+ frameworkLog('error', `Error in handler for ${type}:`, error);
121
123
  }
122
124
  }
123
125
  } catch (error) {
124
- console.error('Error handling message:', error);
126
+ frameworkLog('error', 'Error handling message:', error);
125
127
  }
126
128
  }
127
129
 
@@ -230,7 +232,7 @@ export class WebSocketServer {
230
232
  }));
231
233
  return true;
232
234
  } catch (error) {
233
- console.error(`Error sending to ${connectionId}:`, error);
235
+ frameworkLog('error', `Error sending to ${connectionId}:`, error);
234
236
  return false;
235
237
  }
236
238
  }
@@ -255,7 +257,7 @@ export class WebSocketServer {
255
257
  timestamp
256
258
  }));
257
259
  } catch (error) {
258
- console.error(`Error broadcasting to ${connectionId}:`, error);
260
+ frameworkLog('error', `Error broadcasting to ${connectionId}:`, error);
259
261
  }
260
262
  }
261
263
  }
@@ -269,28 +271,28 @@ export class WebSocketServer {
269
271
  }
270
272
  } else if (scope === 'presence') {
271
273
  // Broadcast presence to all connections
272
- for (const [connId, connInfo] of this.connections.entries()) {
273
- try {
274
+ for (const [connId, connInfo] of this.connections.entries()) {
275
+ try {
274
276
  connInfo.ws.send(JSON.stringify({
275
277
  type,
276
278
  payload,
277
279
  timestamp
278
280
  }));
279
- } catch (error) {
280
- console.error(`Error broadcasting presence to ${connId}:`, error);
281
+ } catch (error) {
282
+ frameworkLog('error', `Error broadcasting presence to ${connId}:`, error);
281
283
  }
282
284
  }
283
285
  } else {
284
286
  // Global broadcast
285
- for (const [connId, connInfo] of this.connections.entries()) {
286
- try {
287
+ for (const [connId, connInfo] of this.connections.entries()) {
288
+ try {
287
289
  connInfo.ws.send(JSON.stringify({
288
290
  type,
289
291
  payload,
290
292
  timestamp
291
293
  }));
292
- } catch (error) {
293
- console.error(`Error broadcasting to ${connId}:`, error);
294
+ } catch (error) {
295
+ frameworkLog('error', `Error broadcasting to ${connId}:`, error);
294
296
  }
295
297
  }
296
298
  }
@@ -504,7 +506,7 @@ export class WebSocketClient {
504
506
  this.ws = new WebSocket(this.url);
505
507
 
506
508
  this.ws.onopen = () => {
507
- console.log('Connected to WebSocket server');
509
+ frameworkLog('log', 'Connected to WebSocket server');
508
510
  this.reconnectAttempts = 0;
509
511
  this.reconnectDelay = 1000;
510
512
  this.emit('connected', { userId: this.userId });
@@ -516,18 +518,18 @@ export class WebSocketClient {
516
518
  };
517
519
 
518
520
  this.ws.onerror = (error) => {
519
- console.error('WebSocket error:', error);
521
+ frameworkLog('error', 'WebSocket error:', error);
520
522
  this.emit('error', error);
521
523
  };
522
524
 
523
525
  this.ws.onclose = () => {
524
- console.log('Disconnected from WebSocket');
526
+ frameworkLog('log', 'Disconnected from WebSocket');
525
527
  this.ws = null;
526
528
  this.isConnecting = false;
527
529
  this.attemptReconnect();
528
530
  };
529
531
  } catch (error) {
530
- console.error('Failed to connect to WebSocket:', error);
532
+ frameworkLog('error', 'Failed to connect to WebSocket:', error);
531
533
  this.isConnecting = false;
532
534
  this.attemptReconnect();
533
535
  }
@@ -547,7 +549,7 @@ export class WebSocketClient {
547
549
  this.reconnectAttempts++;
548
550
  const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), 30000);
549
551
 
550
- console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
552
+ frameworkLog('log', `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
551
553
 
552
554
  setTimeout(() => {
553
555
  this.connect();
@@ -574,7 +576,7 @@ export class WebSocketClient {
574
576
  try {
575
577
  handler(payload);
576
578
  } catch (error) {
577
- console.error(`Error in handler for ${type}:`, error);
579
+ frameworkLog('error', `Error in handler for ${type}:`, error);
578
580
  }
579
581
  }
580
582
  }
@@ -584,7 +586,7 @@ export class WebSocketClient {
584
586
  */
585
587
  send(type, payload) {
586
588
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
587
- console.warn('WebSocket not connected');
589
+ frameworkLog('warn', 'WebSocket not connected');
588
590
  return false;
589
591
  }
590
592
 
@@ -596,7 +598,7 @@ export class WebSocketClient {
596
598
  }));
597
599
  return true;
598
600
  } catch (error) {
599
- console.error('Error sending message:', error);
601
+ frameworkLog('error', 'Error sending message:', error);
600
602
  return false;
601
603
  }
602
604
  }