@opengeni/api-router 0.12.5 → 0.12.12
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/dist/app.js +1 -1
- package/dist/{chunk-3BHOMOSD.js → chunk-5TULDPWX.js} +589 -153
- package/dist/chunk-5TULDPWX.js.map +1 -0
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/app.ts +12 -0
- package/src/github-access.ts +1 -0
- package/src/index.ts +8 -3
- package/src/mcp/files.ts +77 -0
- package/src/mcp/server.ts +196 -86
- package/src/routes/files.ts +11 -0
- package/src/routes/github.ts +325 -20
- package/src/routes/scheduled-tasks.ts +1 -0
- package/src/routes/sessions.ts +12 -23
- package/src/routes/workspace-capture.ts +108 -8
- package/dist/chunk-3BHOMOSD.js.map +0 -1
|
@@ -38,6 +38,97 @@ export type CaptureStoragePort = {
|
|
|
38
38
|
}) => Promise<{ url: string; expiresAt: Date }>;
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
type LoadedManifest = {
|
|
42
|
+
manifest: WorkspaceCaptureManifest;
|
|
43
|
+
byteLength: number;
|
|
44
|
+
stats: WorkspaceCaptureStats;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type ManifestCacheEntry = LoadedManifest & {
|
|
48
|
+
identity: string;
|
|
49
|
+
inlineResponse?: Extract<GetWorkspaceCaptureResponse, { available: true }>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Process-local cache for immutable capture manifests. Capture rows point at a
|
|
54
|
+
* revision-specific object key, so a successfully validated blob is safe to
|
|
55
|
+
* reuse as long as the row identity still matches. Both entry count and bytes
|
|
56
|
+
* are bounded; large signed-manifest responses bypass the cache.
|
|
57
|
+
*/
|
|
58
|
+
export class WorkspaceCaptureManifestCache {
|
|
59
|
+
readonly #entries = new Map<string, ManifestCacheEntry>();
|
|
60
|
+
#retainedBytes = 0;
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
readonly maxEntries = 64,
|
|
64
|
+
readonly maxBytes = 16 * 1024 * 1024,
|
|
65
|
+
) {}
|
|
66
|
+
|
|
67
|
+
get(row: WorkspaceCaptureRow): LoadedManifest | null {
|
|
68
|
+
const key = row.manifestKey;
|
|
69
|
+
if (!key) return null;
|
|
70
|
+
const entry = this.#entries.get(key);
|
|
71
|
+
if (!entry || entry.identity !== manifestIdentity(row)) return null;
|
|
72
|
+
this.#entries.delete(key);
|
|
73
|
+
this.#entries.set(key, entry);
|
|
74
|
+
return entry;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getInlineResponse(
|
|
78
|
+
row: WorkspaceCaptureRow,
|
|
79
|
+
): Extract<GetWorkspaceCaptureResponse, { available: true }> | null {
|
|
80
|
+
const key = row.manifestKey;
|
|
81
|
+
if (!key) return null;
|
|
82
|
+
const entry = this.#entries.get(key);
|
|
83
|
+
if (!entry || entry.identity !== manifestIdentity(row) || !entry.inlineResponse) return null;
|
|
84
|
+
this.#entries.delete(key);
|
|
85
|
+
this.#entries.set(key, entry);
|
|
86
|
+
return entry.inlineResponse;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setInlineResponse(
|
|
90
|
+
row: WorkspaceCaptureRow,
|
|
91
|
+
response: Extract<GetWorkspaceCaptureResponse, { available: true }>,
|
|
92
|
+
): void {
|
|
93
|
+
const key = row.manifestKey;
|
|
94
|
+
if (!key) return;
|
|
95
|
+
const entry = this.#entries.get(key);
|
|
96
|
+
if (!entry || entry.identity !== manifestIdentity(row)) return;
|
|
97
|
+
entry.inlineResponse = response;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
set(row: WorkspaceCaptureRow, loaded: LoadedManifest): void {
|
|
101
|
+
const key = row.manifestKey;
|
|
102
|
+
if (
|
|
103
|
+
!key ||
|
|
104
|
+
this.maxEntries <= 0 ||
|
|
105
|
+
this.maxBytes <= 0 ||
|
|
106
|
+
loaded.byteLength > CAPTURE_INLINE_MANIFEST_MAX_BYTES ||
|
|
107
|
+
loaded.byteLength > this.maxBytes
|
|
108
|
+
) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const previous = this.#entries.get(key);
|
|
112
|
+
if (previous) {
|
|
113
|
+
this.#retainedBytes -= previous.byteLength;
|
|
114
|
+
this.#entries.delete(key);
|
|
115
|
+
}
|
|
116
|
+
this.#entries.set(key, { ...loaded, identity: manifestIdentity(row) });
|
|
117
|
+
this.#retainedBytes += loaded.byteLength;
|
|
118
|
+
while (this.#entries.size > this.maxEntries || this.#retainedBytes > this.maxBytes) {
|
|
119
|
+
const oldestKey = this.#entries.keys().next().value;
|
|
120
|
+
if (oldestKey === undefined) break;
|
|
121
|
+
const oldest = this.#entries.get(oldestKey);
|
|
122
|
+
this.#entries.delete(oldestKey);
|
|
123
|
+
if (oldest) this.#retainedBytes -= oldest.byteLength;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function manifestIdentity(row: WorkspaceCaptureRow): string {
|
|
129
|
+
return `${row.sessionId}\0${row.turnId}\0${row.revision}\0${row.leaseEpoch}\0${row.capturedAt}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
41
132
|
function signedUrl(signed: { url: string; expiresAt: Date }): { url: string; expiresAt: string } {
|
|
42
133
|
return { url: signed.url, expiresAt: signed.expiresAt.toISOString() };
|
|
43
134
|
}
|
|
@@ -51,12 +142,11 @@ function signedUrl(signed: { url: string; expiresAt: Date }): { url: string; exp
|
|
|
51
142
|
async function loadManifest(
|
|
52
143
|
row: WorkspaceCaptureRow,
|
|
53
144
|
storage: CaptureStoragePort,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
byteLength: number;
|
|
57
|
-
stats: WorkspaceCaptureStats;
|
|
58
|
-
} | null> {
|
|
145
|
+
cache?: WorkspaceCaptureManifestCache,
|
|
146
|
+
): Promise<LoadedManifest | null> {
|
|
59
147
|
if (!row.manifestKey) return null;
|
|
148
|
+
const cached = cache?.get(row);
|
|
149
|
+
if (cached) return cached;
|
|
60
150
|
const stats = WorkspaceCaptureStats.safeParse(row.stats);
|
|
61
151
|
if (!stats.success) {
|
|
62
152
|
console.warn(
|
|
@@ -114,7 +204,9 @@ async function loadManifest(
|
|
|
114
204
|
);
|
|
115
205
|
return null;
|
|
116
206
|
}
|
|
117
|
-
|
|
207
|
+
const loaded = { manifest, byteLength: blob.bytes.byteLength, stats: servedStats };
|
|
208
|
+
cache?.set(row, loaded);
|
|
209
|
+
return loaded;
|
|
118
210
|
}
|
|
119
211
|
|
|
120
212
|
/**
|
|
@@ -126,6 +218,7 @@ async function loadManifest(
|
|
|
126
218
|
export async function serveWorkspaceCapture(
|
|
127
219
|
row: WorkspaceCaptureRow | null,
|
|
128
220
|
storage: CaptureStoragePort,
|
|
221
|
+
cache?: WorkspaceCaptureManifestCache,
|
|
129
222
|
): Promise<GetWorkspaceCaptureResponse> {
|
|
130
223
|
if (!row) {
|
|
131
224
|
return { available: false };
|
|
@@ -148,11 +241,13 @@ export async function serveWorkspaceCapture(
|
|
|
148
241
|
});
|
|
149
242
|
}
|
|
150
243
|
if (row.state !== "available" || !row.manifestKey) return { available: false };
|
|
244
|
+
const cachedResponse = cache?.getInlineResponse(row);
|
|
245
|
+
if (cachedResponse) return cachedResponse;
|
|
151
246
|
// Validate every manifest before serving it, including the rare >2MB signed
|
|
152
247
|
// path. Previously that branch signed arbitrary bytes merely because they
|
|
153
248
|
// exceeded the inline cap, allowing a poison/mis-keyed blob to bypass both the
|
|
154
249
|
// schema and row-identity checks.
|
|
155
|
-
const loaded = await loadManifest(row, storage);
|
|
250
|
+
const loaded = await loadManifest(row, storage, cache);
|
|
156
251
|
if (!loaded) return { available: false };
|
|
157
252
|
const meta = {
|
|
158
253
|
available: true as const,
|
|
@@ -164,11 +259,16 @@ export async function serveWorkspaceCapture(
|
|
|
164
259
|
stats: loaded.stats,
|
|
165
260
|
};
|
|
166
261
|
if (loaded.byteLength <= CAPTURE_INLINE_MANIFEST_MAX_BYTES) {
|
|
167
|
-
|
|
262
|
+
const response = GetWorkspaceCaptureResponse.parse({
|
|
168
263
|
...meta,
|
|
169
264
|
manifest: loaded.manifest,
|
|
170
265
|
manifestUrl: null,
|
|
171
266
|
});
|
|
267
|
+
if (!response.available) {
|
|
268
|
+
throw new Error("validated inline capture response lost its available discriminator");
|
|
269
|
+
}
|
|
270
|
+
cache?.setInlineResponse(row, response);
|
|
271
|
+
return response;
|
|
172
272
|
}
|
|
173
273
|
const signed = await storage.createGetUrl({
|
|
174
274
|
key: row.manifestKey,
|