@africode/core 5.0.1 → 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.
- package/COMPONENT_SCHEMA.json +103 -69
- package/components/base.d.ts +1 -1
- package/components/base.js +71 -21
- package/core/a2ui-schema-manager.js +9 -2
- package/core/a2ui.js +131 -43
- package/core/actions.js +27 -0
- package/core/bun-runtime.js +207 -724
- package/core/compliance.js +6 -5
- package/core/config.js +7 -5
- package/core/enhanced-hmr.js +16 -14
- package/core/file-router.js +42 -282
- package/core/hmr.js +8 -7
- package/core/html.d.ts +15 -101
- package/core/html.js +53 -129
- package/core/lipa-namba-journey.js +74 -12
- package/core/logging.js +14 -0
- package/core/middleware.js +82 -0
- package/core/nida-cig-middleware.js +13 -8
- package/core/plugins/index.js +345 -312
- package/core/request-identity.js +44 -0
- package/core/sdk.js +22 -0
- package/core/session-store.js +68 -0
- package/core/state.js +34 -0
- package/core/websocket.js +22 -20
- package/dist/africode.js +108 -112
- package/dist/africode.js.map +6 -6
- package/dist/build-info.json +3 -3
- package/dist/components.js +351 -351
- package/dist/components.js.map +6 -6
- package/package.json +3 -3
- package/scripts/generate-component-schema.js +1 -1
|
@@ -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
|
-
|
|
122
|
+
frameworkLog('error', `Error in handler for ${type}:`, error);
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
125
|
} catch (error) {
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
+
} catch (error) {
|
|
282
|
+
frameworkLog('error', `Error broadcasting presence to ${connId}:`, error);
|
|
281
283
|
}
|
|
282
284
|
}
|
|
283
285
|
} else {
|
|
284
286
|
// Global broadcast
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
521
|
+
frameworkLog('error', 'WebSocket error:', error);
|
|
520
522
|
this.emit('error', error);
|
|
521
523
|
};
|
|
522
524
|
|
|
523
525
|
this.ws.onclose = () => {
|
|
524
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
601
|
+
frameworkLog('error', 'Error sending message:', error);
|
|
600
602
|
return false;
|
|
601
603
|
}
|
|
602
604
|
}
|