@opengeni/api-router 3.2.2 → 3.3.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/dist/app.js +1 -1
- package/dist/{chunk-PQBHZ2YH.js → chunk-6CECHDZP.js} +481 -333
- package/dist/{chunk-PQBHZ2YH.js.map → chunk-6CECHDZP.js.map} +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/sandbox/channel-a.d.ts +10 -1
- package/dist/site-publishing/agent-data-tools.d.ts +5 -5
- package/dist/site-publishing/agent-session.d.ts +3 -2
- package/dist/site-publishing/runtime.d.ts +7 -6
- package/dist/site-publishing/service.d.ts +8 -11
- package/dist/site-publishing/session-request-authority.d.ts +3 -0
- package/package.json +24 -24
- package/src/access-grant-rls.ts +1 -0
- package/src/http/sse.ts +5 -1
- package/src/routes/browser-sessions.ts +61 -17
- package/src/routes/computer-sessions.ts +102 -39
- package/src/routes/insights-usage.ts +22 -0
- package/src/routes/sessions.ts +22 -4
- package/src/routes/site-runtime.ts +62 -20
- package/src/sandbox/channel-a.ts +18 -1
- package/src/sandbox/metrics-ingestion.ts +1 -0
- package/src/site-frame-guard.ts +11 -2
- package/src/site-publishing/agent-data-tools.ts +16 -8
- package/src/site-publishing/agent-session.ts +24 -6
- package/src/site-publishing/runtime.ts +29 -7
- package/src/site-publishing/service.ts +65 -77
- package/src/site-publishing/session-request-authority.ts +14 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
3
|
import {
|
|
3
4
|
normalizeSitesDomain,
|
|
4
5
|
OPENGENI_API_CONTRACT_HEADER,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
siteAgentClamp,
|
|
24
25
|
siteViewerForGrant,
|
|
25
26
|
type ApiRouteDeps,
|
|
27
|
+
type cancelPublishedSiteSession,
|
|
26
28
|
} from "@opengeni/core";
|
|
27
29
|
import {
|
|
28
30
|
bumpSitePublicationGeneration,
|
|
@@ -38,7 +40,7 @@ import {
|
|
|
38
40
|
readSiteRuntimeUsage,
|
|
39
41
|
recordSitePublicationScan,
|
|
40
42
|
getPublishedSite,
|
|
41
|
-
|
|
43
|
+
listSiteRuntimeCancellationCandidates,
|
|
42
44
|
revokeSiteViewerSessions,
|
|
43
45
|
SitePublishingError,
|
|
44
46
|
SITE_RUNTIME_SITE_PRINCIPAL,
|
|
@@ -174,24 +176,20 @@ export const SITE_EFFECT_CONCURRENCY = 4;
|
|
|
174
176
|
const SITE_EFFECT_QUEUE_LIMIT = 2_000;
|
|
175
177
|
|
|
176
178
|
const effectsBySettings = new WeakMap<object, SitePublishingEffects>();
|
|
177
|
-
|
|
179
|
+
type SiteCancellation = (
|
|
180
|
+
input: Parameters<typeof cancelPublishedSiteSession>[1],
|
|
181
|
+
) => Promise<unknown>;
|
|
182
|
+
const cancellationBySettings = new WeakMap<object, SiteCancellation>();
|
|
178
183
|
|
|
179
184
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* session routes. Without it (the operator script) cancellation is skipped.
|
|
185
|
+
* Internal host lifecycle adapter. This never grants the Site service bearer
|
|
186
|
+
* ordinary session access and never impersonates a conversation's viewer.
|
|
183
187
|
*/
|
|
184
|
-
export function
|
|
188
|
+
export function setSitePublishingCancellation(
|
|
185
189
|
settings: ApiRouteDeps["settings"],
|
|
186
|
-
|
|
190
|
+
cancel: SiteCancellation,
|
|
187
191
|
): void {
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function sitePublishingDispatch(
|
|
192
|
-
settings: ApiRouteDeps["settings"],
|
|
193
|
-
): ((request: Request) => Promise<Response>) | null {
|
|
194
|
-
return dispatchBySettings.get(settings) ?? null;
|
|
192
|
+
cancellationBySettings.set(settings, cancel);
|
|
195
193
|
}
|
|
196
194
|
|
|
197
195
|
type SitePublishingEffectsOptions = {
|
|
@@ -643,26 +641,29 @@ export async function siteServiceSessionRequest(
|
|
|
643
641
|
"x-opengeni-site-id": site.id,
|
|
644
642
|
});
|
|
645
643
|
if (site.versionId) headers.set("x-opengeni-site-version", site.versionId);
|
|
646
|
-
|
|
644
|
+
const body = input.body === undefined ? undefined : JSON.stringify(input.body);
|
|
645
|
+
if (body !== undefined) {
|
|
646
|
+
headers.set("content-type", "application/json");
|
|
647
|
+
// Preserve the exact Request carrying verified-human dispatch proof.
|
|
648
|
+
headers.set("content-length", String(Buffer.byteLength(body)));
|
|
649
|
+
}
|
|
647
650
|
return new Request(new URL(input.path, input.origin), {
|
|
648
651
|
method: input.method,
|
|
649
652
|
headers,
|
|
650
|
-
...(
|
|
653
|
+
...(body === undefined ? {} : { body }),
|
|
651
654
|
});
|
|
652
655
|
}
|
|
653
656
|
|
|
654
|
-
/**
|
|
655
|
-
const
|
|
657
|
+
/** Bound each database page, not the complete cancellation operation. */
|
|
658
|
+
const STOP_CONVERSATIONS_PAGE_SIZE = 200;
|
|
656
659
|
|
|
657
660
|
/**
|
|
658
661
|
* Stop a Site's viewers after a takedown, unpublish or narrower access:
|
|
659
662
|
* revoke every site-session now, and after commit cancel the Site's agent
|
|
660
663
|
* conversations that may still be working (in-flight turns and queued
|
|
661
|
-
* messages),
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
* script) the cancellation is skipped and logged; per-conversation caps
|
|
665
|
-
* still bound what running turns can spend.
|
|
664
|
+
* messages), through the native cancel-only lifecycle adapter. The original
|
|
665
|
+
* actor survives queued background execution. No transcript access is granted.
|
|
666
|
+
* Every page is drained, including old conversations; there is no age cutoff.
|
|
666
667
|
*/
|
|
667
668
|
export async function stopSiteViewers(
|
|
668
669
|
deps: SitePublishingDeps,
|
|
@@ -688,68 +689,55 @@ export async function stopSiteViewers(
|
|
|
688
689
|
}
|
|
689
690
|
}
|
|
690
691
|
if (options.cancelConversations === false) return { revokedSessions };
|
|
691
|
-
const
|
|
692
|
-
if (!
|
|
693
|
-
console.warn("[site-publishing] conversation cancel
|
|
692
|
+
const cancel = cancellationBySettings.get(deps.settings);
|
|
693
|
+
if (!cancel) {
|
|
694
|
+
console.warn("[site-publishing] conversation cancel unavailable: no lifecycle adapter", {
|
|
694
695
|
siteId,
|
|
695
696
|
});
|
|
696
697
|
return { revokedSessions };
|
|
697
698
|
}
|
|
698
699
|
const effects = sitePublishingEffects(deps.settings);
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const versionId = (await getPublishedSite(deps.db, scope, siteId))?.currentVersionId ?? null;
|
|
709
|
-
if (!versionId) {
|
|
710
|
-
console.warn("[site-publishing] conversation cancel skipped: the Site has no version", {
|
|
711
|
-
siteId,
|
|
712
|
-
});
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
const operation = crypto.randomUUID();
|
|
716
|
-
for (const sessionId of sessionIds) {
|
|
717
|
-
try {
|
|
718
|
-
const response = await dispatch(
|
|
719
|
-
await siteServiceSessionRequest(
|
|
720
|
-
deps,
|
|
721
|
-
scope,
|
|
722
|
-
{ id: siteId, versionId },
|
|
723
|
-
{
|
|
724
|
-
origin,
|
|
725
|
-
method: "POST",
|
|
726
|
-
path: `/v1/workspaces/${scope.workspaceId}/sessions/${sessionId}/control`,
|
|
727
|
-
body: {
|
|
728
|
-
action: "cancel",
|
|
729
|
-
clientEventId: `site-stop:${operation}:${sessionId}`.slice(0, 200),
|
|
730
|
-
reason: options.reason.slice(0, 500),
|
|
731
|
-
},
|
|
732
|
-
label: "Published Site",
|
|
733
|
-
},
|
|
734
|
-
),
|
|
735
|
-
);
|
|
736
|
-
if (!response.ok)
|
|
737
|
-
console.warn("[site-publishing] could not cancel a Site conversation", {
|
|
738
|
-
siteId,
|
|
739
|
-
sessionId,
|
|
740
|
-
status: response.status,
|
|
741
|
-
body: (await response.text().catch(() => "")).slice(0, 300),
|
|
742
|
-
});
|
|
743
|
-
else await response.body?.cancel().catch(() => undefined);
|
|
744
|
-
} catch (error) {
|
|
745
|
-
console.warn("[site-publishing] could not cancel a Site conversation", {
|
|
700
|
+
const runInContext = AsyncLocalStorage.snapshot();
|
|
701
|
+
const operation = crypto.randomUUID();
|
|
702
|
+
const startedBefore = new Date();
|
|
703
|
+
const run = () =>
|
|
704
|
+
runInContext(async () => {
|
|
705
|
+
let after: string | undefined;
|
|
706
|
+
let failed = 0;
|
|
707
|
+
for (;;) {
|
|
708
|
+
const sessionIds = await listSiteRuntimeCancellationCandidates(deps.db, scope, {
|
|
746
709
|
siteId,
|
|
747
|
-
|
|
748
|
-
|
|
710
|
+
limit: STOP_CONVERSATIONS_PAGE_SIZE,
|
|
711
|
+
startedBefore,
|
|
712
|
+
...(after ? { after } : {}),
|
|
713
|
+
...(options.toolsNarrowedTo ? { toolsNarrowedTo: options.toolsNarrowedTo } : {}),
|
|
749
714
|
});
|
|
715
|
+
if (sessionIds.length === 0) break;
|
|
716
|
+
for (const sessionId of sessionIds) {
|
|
717
|
+
try {
|
|
718
|
+
await cancel({
|
|
719
|
+
...scope,
|
|
720
|
+
siteId,
|
|
721
|
+
sessionId,
|
|
722
|
+
operationKey: `site-stop:${operation}:${sessionId}`,
|
|
723
|
+
reason: options.reason.slice(0, 500),
|
|
724
|
+
});
|
|
725
|
+
} catch (error) {
|
|
726
|
+
failed += 1;
|
|
727
|
+
console.warn("[site-publishing] could not cancel a Site conversation", {
|
|
728
|
+
siteId,
|
|
729
|
+
sessionId,
|
|
730
|
+
error: error instanceof Error ? error.message : String(error),
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
after = sessionIds.at(-1)!;
|
|
750
735
|
}
|
|
751
|
-
|
|
752
|
-
|
|
736
|
+
if (failed > 0)
|
|
737
|
+
throw new Error(`Site lifecycle cancellation failed for ${failed} conversations`);
|
|
738
|
+
});
|
|
739
|
+
// A saturated best-effort effects queue must not silently drop cancellation.
|
|
740
|
+
if (!effects.schedule("cancel conversations", run)) await run();
|
|
753
741
|
return { revokedSessions };
|
|
754
742
|
}
|
|
755
743
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Exact in-process request proof, installed only by the verified Site gateway. */
|
|
2
|
+
const authorities = new WeakMap<Request, () => Promise<void>>();
|
|
3
|
+
|
|
4
|
+
export function bindSiteSessionRequestAuthority(
|
|
5
|
+
request: Request,
|
|
6
|
+
reauthorize: () => Promise<void>,
|
|
7
|
+
): void {
|
|
8
|
+
authorities.set(request, reauthorize);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Periodic SSE reauthorization must check the Site cookie, not only its delegate. */
|
|
12
|
+
export async function reauthorizeSiteSessionRequest(request: Request): Promise<void> {
|
|
13
|
+
await authorities.get(request)?.();
|
|
14
|
+
}
|