web-agent-bridge 2.5.0 → 2.7.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.
- package/README.md +14 -0
- package/package.json +79 -79
- package/public/.well-known/agent-tools.json +180 -0
- package/sdk/package.json +22 -14
- package/server/adapters/index.js +520 -0
- package/server/config/plans.js +367 -0
- package/server/index.js +4 -0
- package/server/middleware/featureGate.js +88 -0
- package/server/migrations/004_agent_os.sql +158 -0
- package/server/migrations/005_marketplace_metering.sql +126 -0
- package/server/observability/failure-analysis.js +337 -0
- package/server/registry/certification.js +271 -0
- package/server/routes/runtime.js +724 -3
- package/server/runtime/replay.js +264 -0
- package/server/runtime/session-engine.js +293 -0
- package/server/security/index.js +13 -0
- package/server/services/hosted-runtime.js +205 -0
- package/server/services/marketplace.js +270 -0
- package/server/services/metering.js +182 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deterministic Replay Engine
|
|
5
|
+
*
|
|
6
|
+
* Records all task inputs/outputs/side-effects for deterministic replay.
|
|
7
|
+
* Enables debugging, testing, and verification of agent workflows.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const { bus } = require('../runtime/event-bus');
|
|
12
|
+
|
|
13
|
+
class ReplayEngine {
|
|
14
|
+
constructor() {
|
|
15
|
+
this._recordings = new Map(); // taskId → Recording
|
|
16
|
+
this._maxRecordings = 5000;
|
|
17
|
+
this._recordingEnabled = true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Start recording a task execution
|
|
22
|
+
*/
|
|
23
|
+
startRecording(taskId, input) {
|
|
24
|
+
if (!this._recordingEnabled) return null;
|
|
25
|
+
|
|
26
|
+
const recording = {
|
|
27
|
+
id: `rec_${crypto.randomBytes(8).toString('hex')}`,
|
|
28
|
+
taskId,
|
|
29
|
+
input: this._deepClone(input),
|
|
30
|
+
steps: [],
|
|
31
|
+
sideEffects: [],
|
|
32
|
+
startedAt: Date.now(),
|
|
33
|
+
completedAt: null,
|
|
34
|
+
output: null,
|
|
35
|
+
error: null,
|
|
36
|
+
checksum: null,
|
|
37
|
+
replayable: true,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
this._recordings.set(taskId, recording);
|
|
41
|
+
this._evict();
|
|
42
|
+
return recording.id;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Record a step in the execution
|
|
47
|
+
*/
|
|
48
|
+
recordStep(taskId, step) {
|
|
49
|
+
const rec = this._recordings.get(taskId);
|
|
50
|
+
if (!rec) return;
|
|
51
|
+
|
|
52
|
+
rec.steps.push({
|
|
53
|
+
index: rec.steps.length,
|
|
54
|
+
type: step.type,
|
|
55
|
+
action: step.action,
|
|
56
|
+
input: this._deepClone(step.input),
|
|
57
|
+
output: this._deepClone(step.output),
|
|
58
|
+
duration: step.duration || 0,
|
|
59
|
+
timestamp: Date.now(),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Record a side effect (network call, DOM mutation, storage write, etc.)
|
|
65
|
+
*/
|
|
66
|
+
recordSideEffect(taskId, effect) {
|
|
67
|
+
const rec = this._recordings.get(taskId);
|
|
68
|
+
if (!rec) return;
|
|
69
|
+
|
|
70
|
+
rec.sideEffects.push({
|
|
71
|
+
index: rec.sideEffects.length,
|
|
72
|
+
type: effect.type, // 'network', 'dom', 'storage', 'event'
|
|
73
|
+
target: effect.target,
|
|
74
|
+
data: this._deepClone(effect.data),
|
|
75
|
+
timestamp: Date.now(),
|
|
76
|
+
reversible: effect.reversible !== false,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Complete a recording
|
|
82
|
+
*/
|
|
83
|
+
completeRecording(taskId, output, error = null) {
|
|
84
|
+
const rec = this._recordings.get(taskId);
|
|
85
|
+
if (!rec) return null;
|
|
86
|
+
|
|
87
|
+
rec.completedAt = Date.now();
|
|
88
|
+
rec.output = this._deepClone(output);
|
|
89
|
+
rec.error = error ? { message: error.message, code: error.code } : null;
|
|
90
|
+
rec.checksum = this._computeChecksum(rec);
|
|
91
|
+
|
|
92
|
+
bus.emit('replay.recording.complete', { taskId, recordingId: rec.id, steps: rec.steps.length });
|
|
93
|
+
return rec;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Replay a recorded task
|
|
98
|
+
* Returns the replay plan (steps to execute) with recorded outputs for verification
|
|
99
|
+
*/
|
|
100
|
+
async replay(taskId, options = {}) {
|
|
101
|
+
const rec = this._recordings.get(taskId);
|
|
102
|
+
if (!rec) throw new Error(`No recording found for task ${taskId}`);
|
|
103
|
+
if (!rec.completedAt) throw new Error('Recording not yet complete');
|
|
104
|
+
|
|
105
|
+
const replayResult = {
|
|
106
|
+
recordingId: rec.id,
|
|
107
|
+
taskId,
|
|
108
|
+
originalInput: rec.input,
|
|
109
|
+
originalOutput: rec.output,
|
|
110
|
+
steps: [],
|
|
111
|
+
match: true,
|
|
112
|
+
verificationMode: options.verify !== false,
|
|
113
|
+
replayedAt: Date.now(),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// In verification mode, run each step and compare outputs
|
|
117
|
+
if (options.executor && options.verify !== false) {
|
|
118
|
+
for (const step of rec.steps) {
|
|
119
|
+
try {
|
|
120
|
+
const replayOutput = await options.executor(step);
|
|
121
|
+
const outputMatch = this._deepEqual(step.output, replayOutput);
|
|
122
|
+
|
|
123
|
+
replayResult.steps.push({
|
|
124
|
+
index: step.index,
|
|
125
|
+
action: step.action,
|
|
126
|
+
originalOutput: step.output,
|
|
127
|
+
replayOutput,
|
|
128
|
+
match: outputMatch,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!outputMatch) {
|
|
132
|
+
replayResult.match = false;
|
|
133
|
+
if (!options.continueOnMismatch) break;
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
replayResult.steps.push({
|
|
137
|
+
index: step.index,
|
|
138
|
+
action: step.action,
|
|
139
|
+
error: err.message,
|
|
140
|
+
match: false,
|
|
141
|
+
});
|
|
142
|
+
replayResult.match = false;
|
|
143
|
+
if (!options.continueOnMismatch) break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
// Dry-run mode: just return the recorded steps
|
|
148
|
+
replayResult.steps = rec.steps.map(s => ({
|
|
149
|
+
index: s.index,
|
|
150
|
+
action: s.action,
|
|
151
|
+
input: s.input,
|
|
152
|
+
output: s.output,
|
|
153
|
+
duration: s.duration,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
bus.emit('replay.completed', { taskId, match: replayResult.match });
|
|
158
|
+
return replayResult;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get recording
|
|
163
|
+
*/
|
|
164
|
+
getRecording(taskId) {
|
|
165
|
+
return this._recordings.get(taskId) || null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* List recordings
|
|
170
|
+
*/
|
|
171
|
+
listRecordings(limit = 50) {
|
|
172
|
+
const all = Array.from(this._recordings.values());
|
|
173
|
+
return all.slice(-limit).reverse().map(r => ({
|
|
174
|
+
id: r.id,
|
|
175
|
+
taskId: r.taskId,
|
|
176
|
+
steps: r.steps.length,
|
|
177
|
+
sideEffects: r.sideEffects.length,
|
|
178
|
+
startedAt: r.startedAt,
|
|
179
|
+
completedAt: r.completedAt,
|
|
180
|
+
hasError: !!r.error,
|
|
181
|
+
checksum: r.checksum,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Compare two recordings
|
|
187
|
+
*/
|
|
188
|
+
diff(taskId1, taskId2) {
|
|
189
|
+
const r1 = this._recordings.get(taskId1);
|
|
190
|
+
const r2 = this._recordings.get(taskId2);
|
|
191
|
+
if (!r1 || !r2) return null;
|
|
192
|
+
|
|
193
|
+
const diffs = [];
|
|
194
|
+
const maxSteps = Math.max(r1.steps.length, r2.steps.length);
|
|
195
|
+
|
|
196
|
+
for (let i = 0; i < maxSteps; i++) {
|
|
197
|
+
const s1 = r1.steps[i];
|
|
198
|
+
const s2 = r2.steps[i];
|
|
199
|
+
|
|
200
|
+
if (!s1 || !s2) {
|
|
201
|
+
diffs.push({ index: i, type: 'missing', in: s1 ? 'recording2' : 'recording1' });
|
|
202
|
+
} else if (!this._deepEqual(s1.output, s2.output)) {
|
|
203
|
+
diffs.push({ index: i, type: 'output_mismatch', action: s1.action, output1: s1.output, output2: s2.output });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
match: diffs.length === 0,
|
|
209
|
+
inputMatch: this._deepEqual(r1.input, r2.input),
|
|
210
|
+
outputMatch: this._deepEqual(r1.output, r2.output),
|
|
211
|
+
diffs,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Enable/disable recording
|
|
217
|
+
*/
|
|
218
|
+
setEnabled(enabled) {
|
|
219
|
+
this._recordingEnabled = enabled;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
getStats() {
|
|
223
|
+
return {
|
|
224
|
+
totalRecordings: this._recordings.size,
|
|
225
|
+
enabled: this._recordingEnabled,
|
|
226
|
+
maxRecordings: this._maxRecordings,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Internal ──
|
|
231
|
+
|
|
232
|
+
_computeChecksum(rec) {
|
|
233
|
+
const data = JSON.stringify({
|
|
234
|
+
input: rec.input,
|
|
235
|
+
steps: rec.steps.map(s => ({ action: s.action, output: s.output })),
|
|
236
|
+
output: rec.output,
|
|
237
|
+
});
|
|
238
|
+
return crypto.createHash('sha256').update(data).digest('hex').slice(0, 16);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_deepClone(obj) {
|
|
242
|
+
if (obj === undefined || obj === null) return obj;
|
|
243
|
+
try {
|
|
244
|
+
return JSON.parse(JSON.stringify(obj));
|
|
245
|
+
} catch {
|
|
246
|
+
return obj;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_deepEqual(a, b) {
|
|
251
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_evict() {
|
|
255
|
+
if (this._recordings.size <= this._maxRecordings) return;
|
|
256
|
+
const keys = Array.from(this._recordings.keys());
|
|
257
|
+
const toRemove = keys.slice(0, keys.length - this._maxRecordings);
|
|
258
|
+
for (const k of toRemove) this._recordings.delete(k);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const replayEngine = new ReplayEngine();
|
|
263
|
+
|
|
264
|
+
module.exports = { ReplayEngine, replayEngine };
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session Engine
|
|
5
|
+
*
|
|
6
|
+
* Manages browser execution sessions with cookies, tokens,
|
|
7
|
+
* and state persistence across navigation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const { bus } = require('../runtime/event-bus');
|
|
12
|
+
|
|
13
|
+
class SessionEngine {
|
|
14
|
+
constructor() {
|
|
15
|
+
this._sessions = new Map(); // sessionId → BrowserSession
|
|
16
|
+
this._cookieJars = new Map(); // sessionId → CookieJar
|
|
17
|
+
this._maxSessions = 500;
|
|
18
|
+
this._defaultTTL = 3600_000; // 1 hour
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a new browser execution session
|
|
23
|
+
*/
|
|
24
|
+
create(config = {}) {
|
|
25
|
+
const sessionId = `sess_${crypto.randomBytes(16).toString('hex')}`;
|
|
26
|
+
|
|
27
|
+
const session = {
|
|
28
|
+
id: sessionId,
|
|
29
|
+
agentId: config.agentId || null,
|
|
30
|
+
siteId: config.siteId || null,
|
|
31
|
+
state: 'active',
|
|
32
|
+
viewport: config.viewport || { width: 1920, height: 1080 },
|
|
33
|
+
userAgent: config.userAgent || null,
|
|
34
|
+
proxy: config.proxy || null,
|
|
35
|
+
localStorage: {},
|
|
36
|
+
sessionStorage: {},
|
|
37
|
+
variables: {}, // Arbitrary key-value store for the agent
|
|
38
|
+
history: [],
|
|
39
|
+
createdAt: Date.now(),
|
|
40
|
+
expiresAt: Date.now() + (config.ttl || this._defaultTTL),
|
|
41
|
+
lastActivity: Date.now(),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
this._sessions.set(sessionId, session);
|
|
45
|
+
this._cookieJars.set(sessionId, new CookieJar());
|
|
46
|
+
this._evict();
|
|
47
|
+
|
|
48
|
+
bus.emit('session.created', { sessionId, agentId: session.agentId, siteId: session.siteId });
|
|
49
|
+
return session;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get session
|
|
54
|
+
*/
|
|
55
|
+
get(sessionId) {
|
|
56
|
+
const session = this._sessions.get(sessionId);
|
|
57
|
+
if (!session) return null;
|
|
58
|
+
if (session.expiresAt < Date.now()) {
|
|
59
|
+
this.destroy(sessionId);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return session;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Update session last activity
|
|
67
|
+
*/
|
|
68
|
+
touch(sessionId) {
|
|
69
|
+
const session = this._sessions.get(sessionId);
|
|
70
|
+
if (session) session.lastActivity = Date.now();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set cookies for a session
|
|
75
|
+
*/
|
|
76
|
+
setCookies(sessionId, cookies) {
|
|
77
|
+
const jar = this._cookieJars.get(sessionId);
|
|
78
|
+
if (!jar) return;
|
|
79
|
+
for (const cookie of cookies) {
|
|
80
|
+
jar.set(cookie);
|
|
81
|
+
}
|
|
82
|
+
this.touch(sessionId);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get cookies for a session/domain
|
|
87
|
+
*/
|
|
88
|
+
getCookies(sessionId, domain = null) {
|
|
89
|
+
const jar = this._cookieJars.get(sessionId);
|
|
90
|
+
if (!jar) return [];
|
|
91
|
+
return jar.getAll(domain);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Set localStorage value
|
|
96
|
+
*/
|
|
97
|
+
setStorage(sessionId, key, value, type = 'local') {
|
|
98
|
+
const session = this._sessions.get(sessionId);
|
|
99
|
+
if (!session) return;
|
|
100
|
+
const store = type === 'session' ? session.sessionStorage : session.localStorage;
|
|
101
|
+
store[key] = value;
|
|
102
|
+
this.touch(sessionId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get localStorage value
|
|
107
|
+
*/
|
|
108
|
+
getStorage(sessionId, key, type = 'local') {
|
|
109
|
+
const session = this._sessions.get(sessionId);
|
|
110
|
+
if (!session) return null;
|
|
111
|
+
const store = type === 'session' ? session.sessionStorage : session.localStorage;
|
|
112
|
+
return store[key] || null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Set variable in session
|
|
117
|
+
*/
|
|
118
|
+
setVariable(sessionId, key, value) {
|
|
119
|
+
const session = this._sessions.get(sessionId);
|
|
120
|
+
if (session) {
|
|
121
|
+
session.variables[key] = value;
|
|
122
|
+
this.touch(sessionId);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get variable from session
|
|
128
|
+
*/
|
|
129
|
+
getVariable(sessionId, key) {
|
|
130
|
+
const session = this._sessions.get(sessionId);
|
|
131
|
+
return session?.variables[key] || null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Record navigation in session history
|
|
136
|
+
*/
|
|
137
|
+
recordNavigation(sessionId, url, title = '') {
|
|
138
|
+
const session = this._sessions.get(sessionId);
|
|
139
|
+
if (!session) return;
|
|
140
|
+
session.history.push({
|
|
141
|
+
url,
|
|
142
|
+
title,
|
|
143
|
+
timestamp: Date.now(),
|
|
144
|
+
index: session.history.length,
|
|
145
|
+
});
|
|
146
|
+
this.touch(sessionId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Export session state (for transfer/persistence)
|
|
151
|
+
*/
|
|
152
|
+
export(sessionId) {
|
|
153
|
+
const session = this._sessions.get(sessionId);
|
|
154
|
+
if (!session) return null;
|
|
155
|
+
return {
|
|
156
|
+
id: session.id,
|
|
157
|
+
agentId: session.agentId,
|
|
158
|
+
siteId: session.siteId,
|
|
159
|
+
viewport: session.viewport,
|
|
160
|
+
userAgent: session.userAgent,
|
|
161
|
+
localStorage: { ...session.localStorage },
|
|
162
|
+
sessionStorage: { ...session.sessionStorage },
|
|
163
|
+
variables: { ...session.variables },
|
|
164
|
+
cookies: this.getCookies(sessionId),
|
|
165
|
+
history: [...session.history],
|
|
166
|
+
createdAt: session.createdAt,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Import session state (restore from exported data)
|
|
172
|
+
*/
|
|
173
|
+
import(data) {
|
|
174
|
+
const session = this.create({
|
|
175
|
+
agentId: data.agentId,
|
|
176
|
+
siteId: data.siteId,
|
|
177
|
+
viewport: data.viewport,
|
|
178
|
+
userAgent: data.userAgent,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
session.localStorage = data.localStorage || {};
|
|
182
|
+
session.sessionStorage = data.sessionStorage || {};
|
|
183
|
+
session.variables = data.variables || {};
|
|
184
|
+
session.history = data.history || [];
|
|
185
|
+
|
|
186
|
+
if (data.cookies) {
|
|
187
|
+
this.setCookies(session.id, data.cookies);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return session;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Destroy session
|
|
195
|
+
*/
|
|
196
|
+
destroy(sessionId) {
|
|
197
|
+
this._sessions.delete(sessionId);
|
|
198
|
+
this._cookieJars.delete(sessionId);
|
|
199
|
+
bus.emit('session.destroyed', { sessionId });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* List sessions
|
|
204
|
+
*/
|
|
205
|
+
list(filters = {}, limit = 50) {
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
let sessions = Array.from(this._sessions.values()).filter(s => s.expiresAt >= now);
|
|
208
|
+
|
|
209
|
+
if (filters.agentId) sessions = sessions.filter(s => s.agentId === filters.agentId);
|
|
210
|
+
if (filters.siteId) sessions = sessions.filter(s => s.siteId === filters.siteId);
|
|
211
|
+
if (filters.state) sessions = sessions.filter(s => s.state === filters.state);
|
|
212
|
+
|
|
213
|
+
return sessions.slice(0, limit).map(s => ({
|
|
214
|
+
id: s.id,
|
|
215
|
+
agentId: s.agentId,
|
|
216
|
+
siteId: s.siteId,
|
|
217
|
+
state: s.state,
|
|
218
|
+
historyLength: s.history.length,
|
|
219
|
+
createdAt: s.createdAt,
|
|
220
|
+
lastActivity: s.lastActivity,
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
getStats() {
|
|
225
|
+
return {
|
|
226
|
+
activeSessions: this._sessions.size,
|
|
227
|
+
maxSessions: this._maxSessions,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_evict() {
|
|
232
|
+
const now = Date.now();
|
|
233
|
+
for (const [id, session] of this._sessions) {
|
|
234
|
+
if (session.expiresAt < now) this.destroy(id);
|
|
235
|
+
}
|
|
236
|
+
if (this._sessions.size > this._maxSessions) {
|
|
237
|
+
const sorted = Array.from(this._sessions.entries())
|
|
238
|
+
.sort((a, b) => a[1].lastActivity - b[1].lastActivity);
|
|
239
|
+
const toRemove = sorted.slice(0, sorted.length - this._maxSessions);
|
|
240
|
+
for (const [id] of toRemove) this.destroy(id);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── Cookie Jar ─────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
class CookieJar {
|
|
248
|
+
constructor() {
|
|
249
|
+
this._cookies = new Map(); // domain:name → cookie
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
set(cookie) {
|
|
253
|
+
const key = `${cookie.domain || '*'}:${cookie.name}`;
|
|
254
|
+
this._cookies.set(key, {
|
|
255
|
+
name: cookie.name,
|
|
256
|
+
value: cookie.value,
|
|
257
|
+
domain: cookie.domain || '*',
|
|
258
|
+
path: cookie.path || '/',
|
|
259
|
+
expires: cookie.expires || null,
|
|
260
|
+
httpOnly: cookie.httpOnly || false,
|
|
261
|
+
secure: cookie.secure || false,
|
|
262
|
+
sameSite: cookie.sameSite || 'Lax',
|
|
263
|
+
setAt: Date.now(),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
get(name, domain = '*') {
|
|
268
|
+
return this._cookies.get(`${domain}:${name}`) || null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
getAll(domain = null) {
|
|
272
|
+
const now = Date.now();
|
|
273
|
+
const result = [];
|
|
274
|
+
for (const cookie of this._cookies.values()) {
|
|
275
|
+
if (cookie.expires && cookie.expires < now) continue;
|
|
276
|
+
if (domain && cookie.domain !== '*' && cookie.domain !== domain) continue;
|
|
277
|
+
result.push({ ...cookie });
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
delete(name, domain = '*') {
|
|
283
|
+
this._cookies.delete(`${domain}:${name}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
clear() {
|
|
287
|
+
this._cookies.clear();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const sessionEngine = new SessionEngine();
|
|
292
|
+
|
|
293
|
+
module.exports = { SessionEngine, CookieJar, sessionEngine };
|
package/server/security/index.js
CHANGED
|
@@ -184,6 +184,19 @@ class AgentIdentity {
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Validate a session token and return session data
|
|
189
|
+
*/
|
|
190
|
+
validateSession(token) {
|
|
191
|
+
const session = this._sessions.get(token);
|
|
192
|
+
if (!session) return null;
|
|
193
|
+
if (Date.now() > session.expiresAt) {
|
|
194
|
+
this._sessions.delete(token);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
return session;
|
|
198
|
+
}
|
|
199
|
+
|
|
187
200
|
/**
|
|
188
201
|
* Cleanup expired sessions
|
|
189
202
|
*/
|