@opengeni/api-router 3.0.1 → 3.1.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/dist/access-grant-rls.d.ts +8 -1
- package/dist/admission-trace.d.ts +8 -0
- package/dist/app.d.ts +50 -3
- package/dist/app.js +10 -1
- package/dist/artifact-catalog-cursor.d.ts +1 -0
- package/dist/attached-browser-viewer.d.ts +11 -0
- package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
- package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +9 -1
- package/dist/auth/managed-auth.d.ts +72 -5
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/auth/new-signups-gate.d.ts +23 -0
- package/dist/auth/signup-funnel-metrics.d.ts +44 -0
- package/dist/azure-live.d.ts +28 -0
- package/dist/browser-action-fences.d.ts +13 -0
- package/dist/browser-controller-authority.d.ts +1 -61
- package/dist/browser-ephemeral.d.ts +5 -0
- package/dist/browser-lost-runtime-end.d.ts +24 -0
- package/dist/browser-state-authority.d.ts +1 -27
- package/dist/chunk-OGQDXVXY.js +18 -0
- package/dist/chunk-OGQDXVXY.js.map +1 -0
- package/dist/{chunk-AJ62T5QN.js → chunk-OT56OY4V.js} +80503 -39869
- package/dist/chunk-OT56OY4V.js.map +1 -0
- package/dist/claude-default-models.d.ts +19 -0
- package/dist/claude-subscription-core.d.ts +50 -0
- package/dist/claude-subscription-oauth.d.ts +36 -0
- package/dist/claude-subscription-profile.d.ts +9 -0
- package/dist/claude-subscription-usage.d.ts +6 -0
- package/dist/claude-workspace-connection.d.ts +15 -0
- package/dist/codemode.d.ts +40 -5
- package/dist/codex-realtime.d.ts +29 -3
- package/dist/codex-session-pointers.d.ts +19 -0
- package/dist/connection-ownership.d.ts +6 -3
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/credit-promotion-snapshot.d.ts +6 -0
- package/dist/editable-artifact-source-session.d.ts +3 -0
- package/dist/editable-artifact-websocket.d.ts +5 -3
- package/dist/fatal-process-boundary.d.ts +9 -1
- package/dist/github-access.d.ts +7 -1
- package/dist/github-browser-flow.d.ts +1 -1
- package/dist/http/acting-person.d.ts +36 -0
- package/dist/http/api-contract-compat.d.ts +19 -0
- package/dist/http/api-error.d.ts +50 -1
- package/dist/http/auth.d.ts +8 -1
- package/dist/http/cors.d.ts +12 -0
- package/dist/http/deprecation.d.ts +42 -0
- package/dist/http/keyed-admission.d.ts +12 -0
- package/dist/http/local-browser-boundary.d.ts +72 -0
- package/dist/http/metrics-listener.d.ts +22 -0
- package/dist/http/path-identifier.d.ts +17 -0
- package/dist/http/registered-route-label.d.ts +16 -0
- package/dist/http/rejection-telemetry.d.ts +34 -0
- package/dist/http/request-body.d.ts +27 -0
- package/dist/http/request-source.d.ts +41 -3
- package/dist/http/sse.d.ts +12 -1
- package/dist/http/unmatched-route.d.ts +18 -0
- package/dist/http/user-content.d.ts +36 -0
- package/dist/http/variable-sets-removed.d.ts +41 -0
- package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
- package/dist/index.js +1279 -108
- package/dist/index.js.map +1 -1
- package/dist/integration-connect-metrics.d.ts +35 -0
- package/dist/integration-endpoint-test.d.ts +20 -0
- package/dist/integrations/api-integrations.d.ts +2 -2
- package/dist/integrations/atlassian.d.ts +8 -3
- package/dist/integrations/fiken.d.ts +3 -1
- package/dist/integrations/github-lens-connect.d.ts +37 -66
- package/dist/integrations/google-drive.d.ts +2 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/marketplace-plugin.d.ts +43 -43
- package/dist/integrations/oauth-client.d.ts +171 -12
- package/dist/integrations/oauth-profiles.d.ts +42 -13
- package/dist/integrations/onepassword-vault-provider.d.ts +9 -0
- package/dist/integrations/slack-bot.d.ts +285 -7
- package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
- package/dist/integrations/slack-interactions.d.ts +89 -10
- package/dist/integrations/slack-mrkdwn.d.ts +41 -0
- package/dist/integrations/slack-routing.d.ts +1 -1
- package/dist/integrations/slack-session-defaults.d.ts +58 -0
- package/dist/integrations/slack-task-file-upload.d.ts +36 -0
- package/dist/integrations/slack-tool-review.d.ts +5 -0
- package/dist/interaction-agent-access.d.ts +13 -8
- package/dist/interaction-agent-version.d.ts +21 -0
- package/dist/interaction-holder-heartbeat.d.ts +5 -0
- package/dist/interaction-metrics.d.ts +27 -2
- package/dist/interaction-route-stages.d.ts +25 -0
- package/dist/interaction-secrets-vault.d.ts +20 -0
- package/dist/interaction-secrets.d.ts +103 -0
- package/dist/mcp/action-catalog-types.d.ts +14 -0
- package/dist/mcp/action-catalog.gen.d.ts +2 -0
- package/dist/mcp/action-descriptions.d.ts +4 -0
- package/dist/mcp/action-schema-imports.gen.d.ts +1 -0
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/child-read-evidence.d.ts +9 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +12 -0
- package/dist/mcp/contract-input.d.ts +13 -0
- package/dist/mcp/documents.d.ts +2 -0
- package/dist/mcp/first-party-tool-permissions.d.ts +3 -606
- package/dist/mcp/notification-tools.d.ts +26 -0
- package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
- package/dist/mcp/scheduled-task-input.d.ts +1309 -0
- package/dist/mcp/scheduled-task-view.d.ts +217 -3
- package/dist/mcp/secret-tools.d.ts +14 -0
- package/dist/mcp/server.d.ts +108 -5
- package/dist/mcp/session-admin-access-tools.d.ts +33 -0
- package/dist/mcp/session-event-errors.d.ts +28 -0
- package/dist/mcp/session-event-view.d.ts +62 -0
- package/dist/mcp/session-view.d.ts +15 -0
- package/dist/mcp/session-wait.d.ts +18 -0
- package/dist/mcp/site-agent-clamp-tools.d.ts +38 -0
- package/dist/mcp/site-data-tools.d.ts +22 -0
- package/dist/mcp/site-publication-tools.d.ts +19 -0
- package/dist/mcp/site-sql-tools.d.ts +27 -0
- package/dist/mcp-oauth.d.ts +37 -4
- package/dist/model-calls/chat-completions.d.ts +87 -0
- package/dist/model-calls/pending-usage-settlements.d.ts +57 -0
- package/dist/model-calls/service.d.ts +36 -0
- package/dist/model-catalog.d.ts +9 -3
- package/dist/native-push-dispatch.d.ts +69 -0
- package/dist/observability.d.ts +3 -1
- package/dist/organization-mcp.d.ts +82 -0
- package/dist/prepared-mcp-actor.d.ts +77 -0
- package/dist/prepared-mcp-permissions.d.ts +8 -0
- package/dist/routes/analytics-consent.d.ts +26 -0
- package/dist/routes/api-integrations.d.ts +11 -0
- package/dist/routes/api-keys.d.ts +7 -7
- package/dist/routes/automations.d.ts +2 -0
- package/dist/routes/billing.d.ts +59 -0
- package/dist/routes/browser-sessions.d.ts +65 -2
- package/dist/routes/chat-completions.d.ts +11 -0
- package/dist/routes/claude-core-usage.d.ts +20 -0
- package/dist/routes/claude-subscription-accounts.d.ts +3 -0
- package/dist/routes/claude-subscription-oauth.d.ts +3 -0
- package/dist/routes/client-errors.d.ts +182 -0
- package/dist/routes/codex-core.d.ts +401 -0
- package/dist/routes/codex.d.ts +64 -2
- package/dist/routes/computer-sessions.d.ts +6 -1
- package/dist/routes/desktop-distribution.d.ts +32 -0
- package/dist/routes/github-browser-fonts.gen.d.ts +2 -0
- package/dist/routes/github-browser-pages.d.ts +27 -0
- package/dist/routes/inbox.d.ts +13 -0
- package/dist/routes/insights-response-cache.d.ts +10 -0
- package/dist/routes/insights-usage.d.ts +12 -0
- package/dist/routes/insights.d.ts +28 -0
- package/dist/routes/interaction-placement-message.d.ts +7 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/model-account-choice.d.ts +13 -0
- package/dist/routes/model-account-settings.d.ts +19 -0
- package/dist/routes/native-app-auth.d.ts +12 -0
- package/dist/routes/organization-integration-policy.d.ts +8 -0
- package/dist/routes/organization-integrations.d.ts +6 -0
- package/dist/routes/organization-mcp-connections.d.ts +3 -0
- package/dist/routes/organization-model-defaults.d.ts +9 -0
- package/dist/routes/organization-service-accounts.d.ts +3 -0
- package/dist/routes/personal-api-keys.d.ts +3 -0
- package/dist/routes/session-admin-access.d.ts +3 -0
- package/dist/routes/session-artifact-associations.d.ts +4 -0
- package/dist/routes/session-history-imports.d.ts +20 -0
- package/dist/routes/sessions.d.ts +38 -2
- package/dist/routes/site-data.d.ts +18 -0
- package/dist/routes/site-publishing.d.ts +5 -0
- package/dist/routes/site-runtime.d.ts +31 -0
- package/dist/routes/site-sql.d.ts +16 -0
- package/dist/routes/slack-bot-access.d.ts +3 -0
- package/dist/routes/subscription-account-pools.d.ts +21 -0
- package/dist/routes/subscription-core-http.d.ts +5 -0
- package/dist/routes/subscription-pool-access.d.ts +37 -0
- package/dist/routes/subscription-pool-core.d.ts +153 -0
- package/dist/routes/supergrok.d.ts +6 -6
- package/dist/routes/usage-allowances.d.ts +12 -0
- package/dist/routes/variable-sets-removed.d.ts +2 -0
- package/dist/routes/vault-providers.d.ts +34 -0
- package/dist/routes/vaults.d.ts +3 -0
- package/dist/routes/workspace-integrations.d.ts +35 -0
- package/dist/routes/workspace-model-providers.d.ts +4 -0
- package/dist/routes/workspaces.d.ts +11 -2
- package/dist/sandbox/agent-update.d.ts +64 -0
- package/dist/sandbox/auth-callout.d.ts +8 -0
- package/dist/sandbox/channel-a.d.ts +15 -1
- package/dist/sandbox/enrollment.d.ts +26 -4
- package/dist/sandbox/machine-audience.d.ts +29 -0
- package/dist/sandbox/machines.d.ts +15 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -2
- package/dist/sandbox/screen-control.d.ts +85 -0
- package/dist/sandbox/viewer.d.ts +5 -1
- package/dist/sandbox-file-artifacts.d.ts +17 -1
- package/dist/scheduled-task-blockers.d.ts +39 -0
- package/dist/scheduled-task-deletion.d.ts +3 -4
- package/dist/scheduled-task-lifecycle.d.ts +14 -0
- package/dist/secret-answers.d.ts +40 -0
- package/dist/session-create-observability.d.ts +5 -0
- package/dist/session-event-view-36GO3IL7.js +528 -0
- package/dist/session-event-view-36GO3IL7.js.map +1 -0
- package/dist/signed-state-secret.d.ts +18 -0
- package/dist/site-agent-guard.d.ts +53 -0
- package/dist/site-data/operations.d.ts +103 -0
- package/dist/site-data/service.d.ts +203 -0
- package/dist/site-frame-guard.d.ts +32 -0
- package/dist/site-publishing/agent-data-tools.d.ts +84 -0
- package/dist/site-publishing/agent-model.d.ts +58 -0
- package/dist/site-publishing/agent-session.d.ts +16 -0
- package/dist/site-publishing/content-scanner.d.ts +31 -0
- package/dist/site-publishing/edge-sync.d.ts +48 -0
- package/dist/site-publishing/gateway-signature.d.ts +56 -0
- package/dist/site-publishing/limits.d.ts +63 -0
- package/dist/site-publishing/page-tool-authority.d.ts +30 -0
- package/dist/site-publishing/platform-usage.d.ts +113 -0
- package/dist/site-publishing/platform.d.ts +21 -0
- package/dist/site-publishing/readiness.d.ts +62 -0
- package/dist/site-publishing/runtime.d.ts +179 -0
- package/dist/site-publishing/service.d.ts +264 -0
- package/dist/site-publishing/session-events.d.ts +22 -0
- package/dist/site-publishing/session-views.d.ts +36 -0
- package/dist/site-publishing/viewer.d.ts +40 -0
- package/dist/site-session-origin.d.ts +31 -6
- package/dist/site-sql/format.d.ts +47 -0
- package/dist/site-sql/gateway.d.ts +50 -0
- package/dist/site-sql/service.d.ts +213 -0
- package/dist/temporal-schedule-sync.d.ts +26 -0
- package/dist/transcription/billing-refusal.d.ts +8 -0
- package/dist/transcription/normalize.d.ts +23 -0
- package/dist/transcription/providers/azure-mai.d.ts +12 -0
- package/dist/transcription/providers/azure-openai.d.ts +4 -0
- package/dist/transcription/providers/openai.d.ts +16 -0
- package/dist/transcription/providers/subscription-core.d.ts +39 -0
- package/dist/transcription/service.d.ts +10 -1
- package/dist/transcription/wav-duration.d.ts +8 -0
- package/dist/workspace-tool-gateway-attestations.d.ts +37 -0
- package/dist/workspace-tool-gateway-observability.d.ts +16 -0
- package/dist/workspace-tool-gateway-secrets.d.ts +27 -0
- package/dist/workspace-tool-gateway.d.ts +102 -6
- package/dist/workspace-tool-target-fence.d.ts +116 -0
- package/dist/workspace-tool-target.d.ts +225 -0
- package/dist/workspace-webhook-dispatch.d.ts +26 -0
- package/dist/xai-subscription-core.d.ts +60 -0
- package/package.json +25 -20
- package/src/access-grant-rls.ts +64 -14
- package/src/admission-trace.ts +14 -0
- package/src/app.ts +1684 -252
- package/src/artifact-catalog-cursor.ts +1 -0
- package/src/attached-browser-viewer.ts +26 -0
- package/src/auth/managed-auth-attempt-context.ts +9 -0
- package/src/auth/managed-auth-rate-limits.ts +252 -0
- package/src/auth/managed-auth-session-adapter.ts +64 -29
- package/src/auth/managed-auth.ts +303 -35
- package/src/auth/managed-email.ts +18 -1
- package/src/auth/managed-sign-in-notifications.ts +81 -0
- package/src/auth/new-signups-gate.ts +49 -0
- package/src/auth/organization-user-setup.ts +3 -3
- package/src/auth/signup-funnel-metrics.ts +164 -0
- package/src/azure-live.ts +103 -0
- package/src/browser-action-fences.ts +45 -0
- package/src/browser-controller-authority.ts +1 -186
- package/src/browser-ephemeral.ts +39 -0
- package/src/browser-lost-runtime-end.ts +71 -0
- package/src/browser-state-authority.ts +1 -236
- package/src/claude-default-models.ts +37 -0
- package/src/claude-subscription-core.ts +154 -0
- package/src/claude-subscription-oauth.ts +352 -0
- package/src/claude-subscription-profile.ts +73 -0
- package/src/claude-subscription-usage.ts +51 -0
- package/src/claude-workspace-connection.ts +82 -0
- package/src/codemode.ts +236 -39
- package/src/codex-realtime.ts +181 -53
- package/src/codex-session-pointers.ts +80 -0
- package/src/company-brain-okf.ts +1 -1
- package/src/connected-machine-computer-access.ts +2 -1
- package/src/connection-ownership.ts +19 -13
- package/src/controller-data-plane.ts +34 -0
- package/src/credit-promotion-snapshot.ts +36 -0
- package/src/editable-artifact-production.ts +10 -0
- package/src/editable-artifact-source-session.ts +123 -0
- package/src/editable-artifact-websocket.ts +16 -3
- package/src/fatal-process-boundary.ts +67 -3
- package/src/gateway-realtime.ts +12 -8
- package/src/github-access.ts +31 -5
- package/src/github-browser-flow.ts +1 -1
- package/src/http/acting-person.ts +105 -0
- package/src/http/api-contract-compat.ts +49 -0
- package/src/http/api-error.ts +210 -2
- package/src/http/auth.ts +68 -4
- package/src/http/cors.ts +37 -5
- package/src/http/deprecation.ts +118 -0
- package/src/http/keyed-admission.ts +29 -0
- package/src/http/local-browser-boundary.ts +343 -0
- package/src/http/metrics-listener.ts +57 -0
- package/src/http/path-identifier.ts +54 -0
- package/src/http/registered-route-label.ts +34 -0
- package/src/http/rejection-telemetry.ts +120 -0
- package/src/http/request-body.ts +127 -0
- package/src/http/request-source.ts +154 -13
- package/src/http/sse.ts +88 -28
- package/src/http/unmatched-route.ts +60 -0
- package/src/http/user-content.ts +158 -0
- package/src/http/variable-sets-removed.ts +122 -0
- package/src/http/websocket-upgrade-dispatch.ts +29 -0
- package/src/index.ts +107 -57
- package/src/integration-connect-metrics.ts +228 -0
- package/src/integration-endpoint-test.ts +291 -0
- package/src/integrations/api-integrations.ts +81 -7
- package/src/integrations/atlassian.ts +202 -83
- package/src/integrations/fiken.ts +66 -38
- package/src/integrations/github-app-connect.ts +64 -18
- package/src/integrations/github-lens-connect.ts +8 -29
- package/src/integrations/google-drive.ts +448 -152
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/marketplace-plugin.ts +1 -1
- package/src/integrations/oauth-client.ts +733 -193
- package/src/integrations/oauth-profiles.ts +116 -45
- package/src/integrations/onepassword-vault-provider.ts +16 -0
- package/src/integrations/personal-github-repositories.ts +1 -1
- package/src/integrations/personal-github.ts +61 -35
- package/src/integrations/provider-oauth.ts +67 -85
- package/src/integrations/slack-app-home.ts +5 -5
- package/src/integrations/slack-bot.ts +893 -201
- package/src/integrations/slack-file-upload-flow.ts +147 -0
- package/src/integrations/slack-install.ts +7 -7
- package/src/integrations/slack-interactions.ts +1589 -299
- package/src/integrations/slack-mrkdwn.ts +193 -0
- package/src/integrations/slack-routing.ts +2 -2
- package/src/integrations/slack-session-defaults.ts +224 -0
- package/src/integrations/slack-task-file-upload.ts +323 -0
- package/src/integrations/slack-tool-review.ts +65 -0
- package/src/integrations/social-oauth.ts +90 -58
- package/src/interaction-agent-access.ts +40 -20
- package/src/interaction-agent-version.ts +85 -0
- package/src/interaction-holder-heartbeat.ts +24 -8
- package/src/interaction-metrics.ts +164 -2
- package/src/interaction-route-stages.ts +94 -0
- package/src/interaction-secrets-vault.ts +213 -0
- package/src/interaction-secrets.ts +426 -0
- package/src/mcp/action-catalog-types.ts +14 -0
- package/src/mcp/action-catalog.gen.ts +8014 -0
- package/src/mcp/action-descriptions.ts +299 -0
- package/src/mcp/action-schema-imports.gen.ts +304 -0
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/child-read-evidence.ts +19 -0
- package/src/mcp/company-profile-agent-admin.ts +13 -0
- package/src/mcp/contract-input.ts +165 -0
- package/src/mcp/documents.ts +9 -2
- package/src/mcp/editable-artifacts.ts +29 -11
- package/src/mcp/files.ts +5 -1
- package/src/mcp/first-party-tool-permissions.ts +7 -285
- package/src/mcp/knowledge-entries.ts +108 -34
- package/src/mcp/notification-tools.ts +480 -0
- package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
- package/src/mcp/scheduled-task-input.ts +89 -0
- package/src/mcp/scheduled-task-view.ts +63 -3
- package/src/mcp/secret-tools.ts +185 -0
- package/src/mcp/server.ts +2213 -1116
- package/src/mcp/session-admin-access-tools.ts +163 -0
- package/src/mcp/session-event-errors.ts +123 -0
- package/src/mcp/session-event-view.ts +395 -13
- package/src/mcp/session-view.ts +3 -0
- package/src/mcp/session-wait.ts +67 -7
- package/src/mcp/site-agent-clamp-tools.ts +140 -0
- package/src/mcp/site-data-tools.ts +339 -0
- package/src/mcp/site-publication-tools.ts +201 -0
- package/src/mcp/site-sql-tools.ts +427 -0
- package/src/mcp-oauth.ts +635 -45
- package/src/model-calls/chat-completions.ts +352 -0
- package/src/model-calls/pending-usage-settlements.ts +125 -0
- package/src/model-calls/service.ts +694 -0
- package/src/model-catalog.ts +68 -17
- package/src/native-push-dispatch.ts +457 -0
- package/src/observability.ts +15 -1
- package/src/organization-mcp.ts +485 -0
- package/src/prepared-mcp-actor.ts +154 -0
- package/src/prepared-mcp-permissions.ts +38 -0
- package/src/routes/analytics-consent.ts +123 -0
- package/src/routes/api-integrations.ts +268 -9
- package/src/routes/api-keys.ts +164 -25
- package/src/routes/artifact-catalog.ts +111 -0
- package/src/routes/automations.ts +163 -90
- package/src/routes/billing.ts +535 -59
- package/src/routes/browser-sessions.ts +966 -342
- package/src/routes/canonical-human-identities.ts +61 -0
- package/src/routes/capabilities.ts +112 -5
- package/src/routes/channels.ts +4 -3
- package/src/routes/chat-completions.ts +235 -0
- package/src/routes/claude-core-usage.ts +147 -0
- package/src/routes/claude-subscription-accounts.ts +233 -0
- package/src/routes/claude-subscription-oauth.ts +175 -0
- package/src/routes/client-errors.ts +394 -0
- package/src/routes/codex-core.ts +687 -0
- package/src/routes/codex.ts +1277 -1119
- package/src/routes/company-brain.ts +4 -0
- package/src/routes/computer-sessions.ts +319 -85
- package/src/routes/connect.ts +277 -175
- package/src/routes/connections.ts +690 -363
- package/src/routes/desktop-distribution.ts +217 -0
- package/src/routes/documents.ts +13 -7
- package/src/routes/editable-artifacts.ts +74 -4
- package/src/routes/enrollments.ts +110 -16
- package/src/routes/external-identity-links.ts +13 -12
- package/src/routes/files.ts +41 -16
- package/src/routes/github-browser-fonts.gen.ts +12 -0
- package/src/routes/github-browser-pages.ts +258 -0
- package/src/routes/github.ts +200 -75
- package/src/routes/inbox.ts +353 -0
- package/src/routes/insights-response-cache.ts +83 -0
- package/src/routes/insights-usage.ts +345 -0
- package/src/routes/insights.ts +103 -8
- package/src/routes/install.ts +51 -10
- package/src/routes/integration-facets.ts +70 -28
- package/src/routes/interaction-placement-message.ts +12 -0
- package/src/routes/knowledge.ts +136 -27
- package/src/routes/machines.ts +189 -135
- package/src/routes/managed-auth-session-sets.ts +43 -10
- package/src/routes/managed-onboarding.ts +39 -19
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/memory-slack-publications.ts +3 -2
- package/src/routes/model-account-choice.ts +194 -0
- package/src/routes/model-account-settings.ts +235 -0
- package/src/routes/model-connection-access.ts +210 -48
- package/src/routes/native-app-auth.ts +296 -0
- package/src/routes/organization-integration-policy.ts +116 -0
- package/src/routes/organization-integrations.ts +326 -0
- package/src/routes/organization-mcp-connections.ts +183 -0
- package/src/routes/organization-memberships.ts +205 -19
- package/src/routes/organization-model-defaults.ts +110 -0
- package/src/routes/organization-model-providers.ts +74 -3
- package/src/routes/organization-service-accounts.ts +128 -0
- package/src/routes/organization-sessions.ts +21 -5
- package/src/routes/personal-api-keys.ts +184 -0
- package/src/routes/personal-github-git-broker.ts +2 -2
- package/src/routes/personal-github.ts +9 -4
- package/src/routes/plugins.ts +46 -7
- package/src/routes/pr-review-github.ts +105 -118
- package/src/routes/pr-review.ts +88 -82
- package/src/routes/preference-registry.ts +18 -0
- package/src/routes/rigs.ts +48 -13
- package/src/routes/scheduled-tasks.ts +347 -27
- package/src/routes/session-admin-access.ts +271 -0
- package/src/routes/session-artifact-associations.ts +164 -0
- package/src/routes/session-history-imports.ts +305 -0
- package/src/routes/sessions.ts +1682 -363
- package/src/routes/site-data.ts +340 -0
- package/src/routes/site-publishing.ts +584 -0
- package/src/routes/site-runtime.ts +1944 -0
- package/src/routes/site-sql.ts +263 -0
- package/src/routes/skill-content.ts +72 -7
- package/src/routes/skills.ts +5 -4
- package/src/routes/slack-bot-access.ts +108 -0
- package/src/routes/social.ts +27 -15
- package/src/routes/subscription-account-pools.ts +209 -0
- package/src/routes/subscription-core-http.ts +18 -0
- package/src/routes/subscription-pool-access.ts +180 -0
- package/src/routes/subscription-pool-core.ts +552 -0
- package/src/routes/supergrok.ts +174 -655
- package/src/routes/transcription-recordings.ts +71 -4
- package/src/routes/transcriptions.ts +26 -2
- package/src/routes/usage-allowances.ts +381 -0
- package/src/routes/variable-sets-removed.ts +19 -0
- package/src/routes/vault-providers.ts +782 -0
- package/src/routes/vaults.ts +421 -0
- package/src/routes/video-generation.ts +30 -12
- package/src/routes/workspace-artifacts.ts +165 -53
- package/src/routes/workspace-integrations.ts +576 -0
- package/src/routes/workspace-model-providers.ts +148 -0
- package/src/routes/workspace-state.ts +4 -0
- package/src/routes/workspaces.ts +625 -117
- package/src/sandbox/agent-update.ts +315 -0
- package/src/sandbox/auth-callout.ts +104 -21
- package/src/sandbox/channel-a.ts +165 -55
- package/src/sandbox/enrollment.ts +131 -22
- package/src/sandbox/machine-audience.ts +192 -0
- package/src/sandbox/machines.ts +75 -31
- package/src/sandbox/metrics-ingestion.ts +133 -2
- package/src/sandbox/rematerialize.ts +51 -21
- package/src/sandbox/screen-control.ts +289 -0
- package/src/sandbox/viewer.ts +33 -58
- package/src/sandbox-file-artifacts.ts +53 -27
- package/src/scheduled-task-blockers.ts +293 -0
- package/src/scheduled-task-deletion.ts +13 -3
- package/src/scheduled-task-lifecycle.ts +84 -0
- package/src/secret-answers.ts +119 -0
- package/src/session-create-observability.ts +46 -0
- package/src/session-realtime-context.ts +26 -4
- package/src/signed-state-secret.ts +58 -0
- package/src/site-agent-guard.ts +261 -0
- package/src/site-data/operations.ts +777 -0
- package/src/site-data/service.ts +1043 -0
- package/src/site-frame-guard.ts +107 -0
- package/src/site-publishing/agent-data-tools.ts +250 -0
- package/src/site-publishing/agent-model.ts +127 -0
- package/src/site-publishing/agent-session.ts +148 -0
- package/src/site-publishing/content-scanner.ts +93 -0
- package/src/site-publishing/edge-sync.ts +148 -0
- package/src/site-publishing/gateway-signature.ts +198 -0
- package/src/site-publishing/limits.ts +315 -0
- package/src/site-publishing/page-tool-authority.ts +111 -0
- package/src/site-publishing/platform-usage.ts +479 -0
- package/src/site-publishing/platform.ts +98 -0
- package/src/site-publishing/readiness.ts +330 -0
- package/src/site-publishing/runtime.ts +831 -0
- package/src/site-publishing/service.ts +1282 -0
- package/src/site-publishing/session-events.ts +262 -0
- package/src/site-publishing/session-views.ts +262 -0
- package/src/site-publishing/viewer.ts +75 -0
- package/src/site-session-origin.ts +91 -6
- package/src/site-sql/format.ts +134 -0
- package/src/site-sql/gateway.ts +168 -0
- package/src/site-sql/service.ts +1636 -0
- package/src/site-uploads.ts +27 -5
- package/src/temporal-schedule-sync.ts +74 -0
- package/src/transcription/billing-refusal.ts +14 -0
- package/src/transcription/normalize.ts +77 -0
- package/src/transcription/providers/azure-mai.ts +98 -0
- package/src/transcription/providers/azure-openai.ts +16 -7
- package/src/transcription/providers/codex-subscription.ts +190 -67
- package/src/transcription/providers/openai.ts +79 -5
- package/src/transcription/providers/subscription-core.ts +125 -0
- package/src/transcription/providers/xai-subscription.ts +81 -83
- package/src/transcription/segmenter.ts +7 -1
- package/src/transcription/service.ts +239 -10
- package/src/transcription/wav-duration.ts +39 -0
- package/src/workspace-deletion.ts +25 -1
- package/src/workspace-tool-gateway-attestations.ts +133 -0
- package/src/workspace-tool-gateway-observability.ts +64 -0
- package/src/workspace-tool-gateway-secrets.ts +104 -0
- package/src/workspace-tool-gateway.ts +921 -103
- package/src/workspace-tool-target-fence.ts +380 -0
- package/src/workspace-tool-target.ts +554 -0
- package/src/workspace-webhook-dispatch.ts +225 -0
- package/src/xai-realtime.ts +85 -81
- package/src/xai-subscription-core.ts +172 -0
- package/dist/browser-auth-broker.d.ts +0 -20
- package/dist/chunk-AJ62T5QN.js.map +0 -1
- package/dist/connection-authority-owner.d.ts +0 -59
- package/dist/routes/connection-authorities.d.ts +0 -3
- package/dist/routes/environments.d.ts +0 -5
- package/dist/routes/host-mcp-bindings.d.ts +0 -5
- package/dist/routes/host-mcp-resolvers.d.ts +0 -3
- package/dist/routes/packs.d.ts +0 -3
- package/dist/session-event-view-MU3HMNV3.js +0 -264
- package/dist/session-event-view-MU3HMNV3.js.map +0 -1
- package/dist/xai-subscription-auth.d.ts +0 -23
- package/src/browser-auth-broker.ts +0 -155
- package/src/connection-authority-owner.ts +0 -61
- package/src/routes/connection-authorities.ts +0 -139
- package/src/routes/environments.ts +0 -388
- package/src/routes/host-mcp-bindings.ts +0 -122
- package/src/routes/host-mcp-resolvers.ts +0 -41
- package/src/routes/packs.ts +0 -784
- package/src/xai-subscription-auth.ts +0 -139
|
@@ -0,0 +1,1944 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
DeleteSessionQueueItemRequest,
|
|
4
|
+
EditSessionQueueItemRequest,
|
|
5
|
+
matchSiteOperation,
|
|
6
|
+
MoveSessionQueueItemRequest,
|
|
7
|
+
SaveComposerDraftRequest,
|
|
8
|
+
SessionControlRequest,
|
|
9
|
+
SiteAbuseReportRequest,
|
|
10
|
+
SiteAuthExchangeRequest,
|
|
11
|
+
SiteDataFileScope,
|
|
12
|
+
SiteDataGetRequest,
|
|
13
|
+
SiteDataQueryRequest,
|
|
14
|
+
SiteDataWriteRequest,
|
|
15
|
+
SiteSqlRunRequest,
|
|
16
|
+
SiteViewAs,
|
|
17
|
+
type SiteLimitProfileKind,
|
|
18
|
+
SITE_GATEWAY_CALLBACK_PATH,
|
|
19
|
+
SITE_RUNTIME_API_PREFIX,
|
|
20
|
+
SITE_TOOL_METHODS,
|
|
21
|
+
SteerSessionQueueItemRequest,
|
|
22
|
+
SubmitComposerDraftRequest,
|
|
23
|
+
ToolGatewayCallRequest,
|
|
24
|
+
ToolGatewayResolveRequest,
|
|
25
|
+
ToolGatewayInvokeRequest,
|
|
26
|
+
type Permission,
|
|
27
|
+
type SiteAuthExchangeResponse,
|
|
28
|
+
type SiteEmbeddedAgent,
|
|
29
|
+
type SiteRuntimeConfig,
|
|
30
|
+
type ToolRef,
|
|
31
|
+
} from "@opengeni/contracts";
|
|
32
|
+
import {
|
|
33
|
+
withHostSessionUsageLimit,
|
|
34
|
+
withSiteSessionOrigin,
|
|
35
|
+
admissibleWorkspaceModel,
|
|
36
|
+
loadWorkspaceModelSelectionInput,
|
|
37
|
+
resolveWorkspaceModelSelection,
|
|
38
|
+
hasPermission,
|
|
39
|
+
type ApiRouteDeps,
|
|
40
|
+
} from "@opengeni/core";
|
|
41
|
+
import {
|
|
42
|
+
consumeSiteAuthCode,
|
|
43
|
+
finishSiteRuntimeAdmission,
|
|
44
|
+
withSiteRuntimeAdmission,
|
|
45
|
+
createSiteAbuseReport,
|
|
46
|
+
getSiteWorkspaceMemberPermissions,
|
|
47
|
+
readSitePlatformUsage,
|
|
48
|
+
recordSitePlatformRequests,
|
|
49
|
+
setSiteViewerSessionViewAs,
|
|
50
|
+
createSiteViewerSession,
|
|
51
|
+
getOwnedSiteRuntimeConversation,
|
|
52
|
+
getServableSiteVersion,
|
|
53
|
+
getPublishedSiteVersion,
|
|
54
|
+
getSession,
|
|
55
|
+
isSiteWorkspaceMember,
|
|
56
|
+
listOwnedSiteRuntimeConversations,
|
|
57
|
+
listSessionEvents,
|
|
58
|
+
releaseSiteRuntimeConversationStart,
|
|
59
|
+
reserveSiteRuntimeConversationStart,
|
|
60
|
+
reserveSiteRuntimeConversationWork,
|
|
61
|
+
revokeSiteViewerSession,
|
|
62
|
+
SiteDataError,
|
|
63
|
+
SITE_RUNTIME_SITE_PRINCIPAL,
|
|
64
|
+
type SiteRuntimeReservation,
|
|
65
|
+
type SiteRuntimeConversationRecord,
|
|
66
|
+
} from "@opengeni/db";
|
|
67
|
+
import {
|
|
68
|
+
SITE_AGENT_DATA_TOOL_PERMISSIONS,
|
|
69
|
+
siteAgentDataToolsForStart,
|
|
70
|
+
siteAgentInstructions,
|
|
71
|
+
sitePublishedDataViewer,
|
|
72
|
+
} from "../site-publishing/agent-data-tools";
|
|
73
|
+
import {
|
|
74
|
+
normalizeSiteAgentSession,
|
|
75
|
+
SITE_AGENT_DEFAULTS_VERSION,
|
|
76
|
+
} from "../site-publishing/agent-session";
|
|
77
|
+
import {
|
|
78
|
+
isSubscriptionBilledModel,
|
|
79
|
+
resolveSiteOutsideModel,
|
|
80
|
+
siteCreditModelIds,
|
|
81
|
+
} from "../site-publishing/agent-model";
|
|
82
|
+
import {
|
|
83
|
+
afterSitePlatformUsage,
|
|
84
|
+
requireSitePlatformOpen,
|
|
85
|
+
sitePlatformBrakeEngaged,
|
|
86
|
+
} from "../site-publishing/platform-usage";
|
|
87
|
+
import { SiteSqlError } from "../site-sql/service";
|
|
88
|
+
import {
|
|
89
|
+
authorizePublishedSiteTools,
|
|
90
|
+
publishedSiteToolAuthority,
|
|
91
|
+
} from "../site-publishing/page-tool-authority";
|
|
92
|
+
import {
|
|
93
|
+
callWorkspaceToolGateway,
|
|
94
|
+
prepareWorkspaceToolGateway,
|
|
95
|
+
requireWorkspaceSiteToolAuthorization,
|
|
96
|
+
workspaceToolGatewayDeclarations,
|
|
97
|
+
} from "../workspace-tool-gateway";
|
|
98
|
+
import { invokeWorkspaceToolTarget, resolveWorkspaceToolTarget } from "../workspace-tool-target";
|
|
99
|
+
import { withSiteSecretContext } from "../workspace-tool-gateway-secrets";
|
|
100
|
+
import type { Context, Hono } from "hono";
|
|
101
|
+
import { z } from "zod";
|
|
102
|
+
import { markLocalInternalDispatch } from "../http/local-browser-boundary";
|
|
103
|
+
import { USER_CONTENT_SECURITY_HEADERS } from "../http/user-content";
|
|
104
|
+
import {
|
|
105
|
+
getSiteDataFileUrl,
|
|
106
|
+
getSiteDataRecord,
|
|
107
|
+
listSiteDataFiles,
|
|
108
|
+
querySiteData,
|
|
109
|
+
uploadSiteDataFile,
|
|
110
|
+
writeSiteData,
|
|
111
|
+
deleteSiteDataFile,
|
|
112
|
+
} from "../site-data/operations";
|
|
113
|
+
import type { PublishedSiteDataViewer } from "../site-data/service";
|
|
114
|
+
import {
|
|
115
|
+
sameSha256Hex,
|
|
116
|
+
verifySiteGatewayRequest,
|
|
117
|
+
type SiteGatewayFields,
|
|
118
|
+
} from "../site-publishing/gateway-signature";
|
|
119
|
+
import {
|
|
120
|
+
capSiteConversationUsageLimit,
|
|
121
|
+
liftSiteConversationUsageLimit,
|
|
122
|
+
enforceSiteRuntimeRate,
|
|
123
|
+
enforceSiteRuntimeViewerRate,
|
|
124
|
+
findSiteRuntimeHost,
|
|
125
|
+
meterSiteRuntime,
|
|
126
|
+
requireServedSiteRuntimeHost,
|
|
127
|
+
requireSiteAgent,
|
|
128
|
+
resolveSiteRuntimeViewer,
|
|
129
|
+
settleSiteRuntimeSpend,
|
|
130
|
+
sha256Text,
|
|
131
|
+
siteReservationRefused,
|
|
132
|
+
siteClientAddressPrincipal,
|
|
133
|
+
siteNotAvailableForViewer,
|
|
134
|
+
siteRuntimeBudgetKey,
|
|
135
|
+
siteRuntimeBudgetLimits,
|
|
136
|
+
siteViewerCapabilities,
|
|
137
|
+
siteRuntimeForwardedResponse,
|
|
138
|
+
siteRuntimeLimitProfile,
|
|
139
|
+
siteRuntimeSessionRequest,
|
|
140
|
+
siteWideBudget,
|
|
141
|
+
sweepSiteRuntimeReservations,
|
|
142
|
+
type SiteRuntimeHost,
|
|
143
|
+
} from "../site-publishing/runtime";
|
|
144
|
+
import {
|
|
145
|
+
configuredSitesDomain,
|
|
146
|
+
healSiteEdgeIfStale,
|
|
147
|
+
notifySiteAbuse,
|
|
148
|
+
setSitePublishingDispatch,
|
|
149
|
+
siteHostMapRecord,
|
|
150
|
+
sitePublishingError,
|
|
151
|
+
} from "../site-publishing/service";
|
|
152
|
+
import {
|
|
153
|
+
siteSessionEventPage,
|
|
154
|
+
siteSessionEventPageResponse,
|
|
155
|
+
siteSessionEventStreamResponse,
|
|
156
|
+
SITE_VISIBLE_SESSION_EVENT_TYPES,
|
|
157
|
+
} from "../site-publishing/session-events";
|
|
158
|
+
import {
|
|
159
|
+
siteAcceptedEventView,
|
|
160
|
+
siteAcceptedMessageView,
|
|
161
|
+
siteControlResultView,
|
|
162
|
+
siteControlView,
|
|
163
|
+
siteDraftView,
|
|
164
|
+
siteHumanInputListView,
|
|
165
|
+
siteHumanInputRequestView,
|
|
166
|
+
siteQueueMutationView,
|
|
167
|
+
siteQueueView,
|
|
168
|
+
} from "../site-publishing/session-views";
|
|
169
|
+
import {
|
|
170
|
+
effectiveSiteAccess,
|
|
171
|
+
publicSiteViewer,
|
|
172
|
+
signedInSiteViewerKind,
|
|
173
|
+
type SiteRuntimeViewer,
|
|
174
|
+
} from "../site-publishing/viewer";
|
|
175
|
+
import { runSiteSqlQuery } from "../site-sql/service";
|
|
176
|
+
import { siteDataHttpError } from "./site-data";
|
|
177
|
+
import { runSiteSqlRoute } from "./site-sql";
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* `/v1/site-runtime/*`: the API a Site gateway calls for one published Site
|
|
181
|
+
* host. Nothing else may call it: every request must carry a fresh gateway
|
|
182
|
+
* signature over its method, host, path, body and viewer headers, and must
|
|
183
|
+
* not carry Opengeni credentials. The signature is checked before anything
|
|
184
|
+
* else; then the host names exactly one Site, the viewer is resolved for it,
|
|
185
|
+
* and only the operations in `SITE_OPERATIONS` (plus the gateway's own host,
|
|
186
|
+
* version, sign-in and report calls) are served.
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
/** Sign-in exchanges one client address may attempt per minute (per Site). */
|
|
190
|
+
const EXCHANGES_PER_MINUTE = 30;
|
|
191
|
+
/** Abuse reports one Site accepts per minute from everyone together. */
|
|
192
|
+
const SITE_REPORTS_PER_MINUTE = 60;
|
|
193
|
+
/** Conversations a viewer's list shows. */
|
|
194
|
+
const LIST_LIMIT = 20;
|
|
195
|
+
const MESSAGE_MAX_CHARS = 20_000;
|
|
196
|
+
/** Request bodies site-runtime reads at most (the gateway enforces the same caps). */
|
|
197
|
+
export const SITE_RUNTIME_JSON_BODY_LIMIT = 4 * 1024 * 1024;
|
|
198
|
+
export const SITE_RUNTIME_FILE_BODY_LIMIT = 25 * 1024 * 1024;
|
|
199
|
+
const FILE_UPLOAD_SUFFIX = "/published-artifacts/current/files";
|
|
200
|
+
/** SHA-256 of the empty body: what the gateway signs for GET and HEAD. */
|
|
201
|
+
const EMPTY_BODY_SHA256 = createHash("sha256").digest("hex");
|
|
202
|
+
/** A version a publish or rollback replaced is still served this long (open pages keep loading). */
|
|
203
|
+
export const SITE_VERSION_GRACE_SECONDS = 600;
|
|
204
|
+
|
|
205
|
+
/** The document policy for a published version: scripts in an opaque origin, framed only by its own shell. */
|
|
206
|
+
export const SITE_VERSION_CONTENT_SECURITY_POLICY =
|
|
207
|
+
"sandbox allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads allow-modals; frame-ancestors 'self'";
|
|
208
|
+
|
|
209
|
+
const VersionId = z.string().uuid();
|
|
210
|
+
const ClientEventId = z.string().min(1).max(200).optional();
|
|
211
|
+
const CreateConversationRequest = z
|
|
212
|
+
.object({
|
|
213
|
+
initialMessage: z.string().trim().min(1).max(MESSAGE_MAX_CHARS),
|
|
214
|
+
idempotencyKey: z.string().min(1).max(200).optional(),
|
|
215
|
+
})
|
|
216
|
+
.passthrough();
|
|
217
|
+
const SendMessageRequest = z
|
|
218
|
+
.object({
|
|
219
|
+
type: z.literal("user.message"),
|
|
220
|
+
clientEventId: ClientEventId,
|
|
221
|
+
payload: z.object({ text: z.string().trim().min(1).max(MESSAGE_MAX_CHARS) }).passthrough(),
|
|
222
|
+
})
|
|
223
|
+
.passthrough();
|
|
224
|
+
const SteerRequest = z
|
|
225
|
+
.object({
|
|
226
|
+
text: z.string().trim().min(1).max(MESSAGE_MAX_CHARS),
|
|
227
|
+
clientEventId: ClientEventId,
|
|
228
|
+
})
|
|
229
|
+
.passthrough();
|
|
230
|
+
const SessionMcpServerIdPattern = /^[A-Za-z0-9_-]{1,200}$/u;
|
|
231
|
+
|
|
232
|
+
function invalid(message: string) {
|
|
233
|
+
return sitePublishingError(400, "validation_failed", "site_invalid_request", message);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function unsupported() {
|
|
237
|
+
return sitePublishingError(
|
|
238
|
+
404,
|
|
239
|
+
"not_found",
|
|
240
|
+
"site_operation_unsupported",
|
|
241
|
+
"This operation is not available to published Sites",
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function parseJson<S extends z.ZodType>(bytes: Uint8Array, schema: S): z.infer<S> {
|
|
246
|
+
let value: unknown = null;
|
|
247
|
+
try {
|
|
248
|
+
value = bytes.byteLength === 0 ? null : JSON.parse(new TextDecoder().decode(bytes));
|
|
249
|
+
} catch {
|
|
250
|
+
throw invalid("The request body must be JSON");
|
|
251
|
+
}
|
|
252
|
+
const parsed = schema.safeParse(value);
|
|
253
|
+
if (!parsed.success)
|
|
254
|
+
throw invalid(
|
|
255
|
+
parsed.error.issues
|
|
256
|
+
.slice(0, 3)
|
|
257
|
+
.map((issue) => `${issue.path.join(".") || "request"}: ${issue.message}`)
|
|
258
|
+
.join("; "),
|
|
259
|
+
);
|
|
260
|
+
return parsed.data;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Site Data for a viewer: what the viewer's audience may not use answers
|
|
265
|
+
* `not_available_for_viewer`; other refusals (a record the viewer does not
|
|
266
|
+
* own, a manager-only write) keep their Site Data code.
|
|
267
|
+
*/
|
|
268
|
+
async function runSiteData<T>(fn: () => Promise<T>): Promise<T> {
|
|
269
|
+
try {
|
|
270
|
+
return await fn();
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (
|
|
273
|
+
error instanceof SiteDataError &&
|
|
274
|
+
error.code === "forbidden" &&
|
|
275
|
+
error.details.reason === "guest_access"
|
|
276
|
+
)
|
|
277
|
+
throw siteNotAvailableForViewer("site_data", error.message, error.details);
|
|
278
|
+
if (error instanceof SiteDataError) throw siteDataHttpError(error);
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Site SQL for a viewer: refusals for this viewer answer `not_available_for_viewer`. */
|
|
284
|
+
async function runSiteSqlForViewer<T>(fn: () => Promise<T>): Promise<T> {
|
|
285
|
+
try {
|
|
286
|
+
return await fn();
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (error instanceof SiteSqlError && error.code === "forbidden")
|
|
289
|
+
throw siteNotAvailableForViewer("site_sql", error.message, error.details);
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Hosts of one gateway platform report recorded at a time. */
|
|
295
|
+
const SITE_PLATFORM_REPORT_CONCURRENCY = 8;
|
|
296
|
+
|
|
297
|
+
/** POST /v1/site-runtime/platform/requests (gateway only, signed on the Sites apex). */
|
|
298
|
+
const SitePlatformRequestReport = z.object({
|
|
299
|
+
v: z.literal(1),
|
|
300
|
+
reportId: z.string().regex(/^[A-Za-z0-9_-]{8,64}$/u),
|
|
301
|
+
hosts: z
|
|
302
|
+
.array(
|
|
303
|
+
z.object({
|
|
304
|
+
host: z.string().min(1).max(253),
|
|
305
|
+
requests: z.number().int().positive().max(1_000_000_000),
|
|
306
|
+
}),
|
|
307
|
+
)
|
|
308
|
+
.min(1)
|
|
309
|
+
.max(500),
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
function sameText(a: string, b: string): boolean {
|
|
313
|
+
const left = Buffer.from(a, "utf8");
|
|
314
|
+
const right = Buffer.from(b, "utf8");
|
|
315
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function tooLarge(limit: number) {
|
|
319
|
+
return sitePublishingError(
|
|
320
|
+
413,
|
|
321
|
+
"limit_exceeded",
|
|
322
|
+
"site_request_too_large",
|
|
323
|
+
"This request body is too large",
|
|
324
|
+
{ limitBytes: limit },
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Read a request body of at most `limit` bytes, hashing it as it arrives: a
|
|
330
|
+
* larger declared length is refused before reading, and the stream is cut
|
|
331
|
+
* off once it passes the cap.
|
|
332
|
+
*/
|
|
333
|
+
async function readCappedBody(
|
|
334
|
+
request: Request,
|
|
335
|
+
limit: number,
|
|
336
|
+
): Promise<{ body: Uint8Array; sha256: string }> {
|
|
337
|
+
const hash = createHash("sha256");
|
|
338
|
+
const declared = request.headers.get("content-length")?.trim();
|
|
339
|
+
if (declared && (!/^\d{1,15}$/u.test(declared) || Number(declared) > limit))
|
|
340
|
+
throw tooLarge(limit);
|
|
341
|
+
if (!request.body) return { body: new Uint8Array(), sha256: hash.digest("hex") };
|
|
342
|
+
const reader = request.body.getReader();
|
|
343
|
+
const chunks: Uint8Array[] = [];
|
|
344
|
+
let total = 0;
|
|
345
|
+
try {
|
|
346
|
+
for (;;) {
|
|
347
|
+
const { done, value } = await reader.read();
|
|
348
|
+
if (done) break;
|
|
349
|
+
total += value.byteLength;
|
|
350
|
+
if (total > limit) {
|
|
351
|
+
await reader.cancel().catch(() => undefined);
|
|
352
|
+
throw tooLarge(limit);
|
|
353
|
+
}
|
|
354
|
+
hash.update(value);
|
|
355
|
+
chunks.push(value);
|
|
356
|
+
}
|
|
357
|
+
} finally {
|
|
358
|
+
reader.releaseLock();
|
|
359
|
+
}
|
|
360
|
+
const body = new Uint8Array(total);
|
|
361
|
+
let offset = 0;
|
|
362
|
+
for (const chunk of chunks) {
|
|
363
|
+
body.set(chunk, offset);
|
|
364
|
+
offset += chunk.byteLength;
|
|
365
|
+
}
|
|
366
|
+
return { body, sha256: hash.digest("hex") };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Fields of a session a Site viewer sees: the conversation, not the workspace behind it. */
|
|
370
|
+
const SITE_SESSION_FIELDS = [
|
|
371
|
+
"id",
|
|
372
|
+
"status",
|
|
373
|
+
"title",
|
|
374
|
+
"titleSource",
|
|
375
|
+
"initialMessage",
|
|
376
|
+
"model",
|
|
377
|
+
"reasoningEffort",
|
|
378
|
+
"latencyMode",
|
|
379
|
+
"parentSessionId",
|
|
380
|
+
"activeTurnId",
|
|
381
|
+
"initialTurnId",
|
|
382
|
+
"lastSequence",
|
|
383
|
+
"queueVersion",
|
|
384
|
+
"queueHeadPosition",
|
|
385
|
+
"queueTailPosition",
|
|
386
|
+
"effectiveControl",
|
|
387
|
+
"inputWait",
|
|
388
|
+
"activelyWorking",
|
|
389
|
+
"requiresActionSince",
|
|
390
|
+
"createdAt",
|
|
391
|
+
"updatedAt",
|
|
392
|
+
] as const;
|
|
393
|
+
const SITE_USAGE_LIMIT_FIELDS = [
|
|
394
|
+
"basis",
|
|
395
|
+
"rootSessionId",
|
|
396
|
+
"inherited",
|
|
397
|
+
"status",
|
|
398
|
+
"limitMicros",
|
|
399
|
+
"usedMicros",
|
|
400
|
+
"coverage",
|
|
401
|
+
] as const;
|
|
402
|
+
|
|
403
|
+
function pick(value: Record<string, unknown>, keys: readonly string[]): Record<string, unknown> {
|
|
404
|
+
const out: Record<string, unknown> = {};
|
|
405
|
+
for (const key of keys) if (key in value) out[key] = value[key];
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* The Site-safe projection of a session: no account or workspace ids,
|
|
411
|
+
* instructions, tools or policies, metadata, creator, sandbox or diagnostics.
|
|
412
|
+
*/
|
|
413
|
+
export function siteSessionProjection(session: unknown): Record<string, unknown> {
|
|
414
|
+
if (!session || typeof session !== "object" || Array.isArray(session)) return {};
|
|
415
|
+
const value = session as Record<string, unknown>;
|
|
416
|
+
const projected = pick(value, SITE_SESSION_FIELDS);
|
|
417
|
+
if ("effectiveControl" in projected)
|
|
418
|
+
projected.effectiveControl = siteControlView(projected.effectiveControl);
|
|
419
|
+
const usage = value.usageLimit;
|
|
420
|
+
if (usage && typeof usage === "object" && !Array.isArray(usage))
|
|
421
|
+
projected.usageLimit = pick(usage as Record<string, unknown>, SITE_USAGE_LIMIT_FIELDS);
|
|
422
|
+
return projected;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Query parameters a Site viewer may pass to an event page (anything else is
|
|
427
|
+
* dropped). Class filters and `latest` are not offered: they select hidden
|
|
428
|
+
* event types by class, and the Site projection hides those anyway.
|
|
429
|
+
*/
|
|
430
|
+
const EVENT_QUERY = ["after", "before", "direction", "excludeTypes", "compact"] as const;
|
|
431
|
+
/** Monitoring-read page sizes: the default, and the most one page returns. */
|
|
432
|
+
const SITE_EVENT_PAGE_DEFAULT = 40;
|
|
433
|
+
const SITE_EVENT_PAGE_MAX = 500;
|
|
434
|
+
|
|
435
|
+
function allowedQuery(url: URL, names: readonly string[], extra: Record<string, string> = {}) {
|
|
436
|
+
const params = new URLSearchParams();
|
|
437
|
+
for (const name of names) {
|
|
438
|
+
const value = url.searchParams.get(name);
|
|
439
|
+
if (value !== null) params.set(name, value);
|
|
440
|
+
}
|
|
441
|
+
for (const [name, value] of Object.entries(extra)) params.set(name, value);
|
|
442
|
+
const text = params.toString();
|
|
443
|
+
return text ? `?${text}` : "";
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* The workspace tools a Site conversation gets: the OVERLAP of what the Site
|
|
448
|
+
* declares (its admin-chosen MCP connections) and what the viewer may do in
|
|
449
|
+
* the workspace. Only workspace members who may attach MCP connections to
|
|
450
|
+
* their own sessions (`mcp_servers:attach`, or workspace admins) get them.
|
|
451
|
+
* People outside the workspace (invited people and public visitors, signed in
|
|
452
|
+
* or anonymous) never get workspace tools: only the Site's own data and the
|
|
453
|
+
* page. There is no setting that changes this; `guestTools` is ignored.
|
|
454
|
+
* Optional references: a connection the workspace does not have is skipped
|
|
455
|
+
* rather than failing the conversation.
|
|
456
|
+
*/
|
|
457
|
+
export function siteAgentToolRefs(
|
|
458
|
+
agent: Pick<SiteEmbeddedAgent, "tools">,
|
|
459
|
+
viewer: Pick<SiteRuntimeViewer, "kind">,
|
|
460
|
+
memberPermissions: readonly string[] | null = null,
|
|
461
|
+
): ToolRef[] {
|
|
462
|
+
const toolIds = siteViewerGetsTools(viewer, memberPermissions) ? (agent.tools ?? []) : [];
|
|
463
|
+
return toolIds
|
|
464
|
+
.filter((tool) => SessionMcpServerIdPattern.test(tool))
|
|
465
|
+
.map((tool) => ({ kind: "mcp", id: tool, optional: true }));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Only workspace members who may use MCP connections get the Site's tools. */
|
|
469
|
+
export function siteViewerGetsTools(
|
|
470
|
+
viewer: Pick<SiteRuntimeViewer, "kind">,
|
|
471
|
+
memberPermissions: readonly string[] | null,
|
|
472
|
+
): boolean {
|
|
473
|
+
if (viewer.kind !== "member" || !memberPermissions) return false;
|
|
474
|
+
return (
|
|
475
|
+
memberPermissions.includes("mcp_servers:attach") ||
|
|
476
|
+
memberPermissions.includes("workspace:admin")
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** A session id derived from the viewer's create idempotency key (same key, same conversation). */
|
|
481
|
+
function idempotentSessionId(seed: string): string {
|
|
482
|
+
const hex = createHash("sha256").update(seed, "utf8").digest("hex");
|
|
483
|
+
const variant = ((parseInt(hex[16]!, 16) & 0x3) | 0x8).toString(16);
|
|
484
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-${variant}${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
type RuntimeRequest = {
|
|
488
|
+
context: Context;
|
|
489
|
+
url: URL;
|
|
490
|
+
method: string;
|
|
491
|
+
suffix: string;
|
|
492
|
+
body: Uint8Array;
|
|
493
|
+
fields: SiteGatewayFields;
|
|
494
|
+
domain: string;
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
export function registerSiteRuntimeRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
498
|
+
const dispatch = async (request: Request): Promise<Response> =>
|
|
499
|
+
await app.fetch(markLocalInternalDispatch(request));
|
|
500
|
+
// Publication changes cancel a Site's conversations through this dispatch.
|
|
501
|
+
setSitePublishingDispatch(deps.settings, dispatch);
|
|
502
|
+
|
|
503
|
+
const handler = async (context: Context): Promise<Response> => {
|
|
504
|
+
const domain = configuredSitesDomain(deps.settings);
|
|
505
|
+
if (!domain || !deps.settings.sitesGatewaySecret)
|
|
506
|
+
throw sitePublishingError(
|
|
507
|
+
404,
|
|
508
|
+
"not_found",
|
|
509
|
+
"sites_publishing_disabled",
|
|
510
|
+
"Publishing Sites is not enabled on this deployment",
|
|
511
|
+
);
|
|
512
|
+
const url = new URL(context.req.url);
|
|
513
|
+
const method = context.req.method.toUpperCase();
|
|
514
|
+
const secrets = [deps.settings.sitesGatewaySecret, deps.settings.sitesGatewaySecretPrevious];
|
|
515
|
+
const refuse = (reason: string) =>
|
|
516
|
+
sitePublishingError(
|
|
517
|
+
401,
|
|
518
|
+
"unauthenticated",
|
|
519
|
+
"site_gateway_signature_invalid",
|
|
520
|
+
"This request was not signed by the Site gateway",
|
|
521
|
+
{ reason },
|
|
522
|
+
);
|
|
523
|
+
// The gateway signature first: nothing about the request is trusted before
|
|
524
|
+
// it. It is checked from the headers alone (the gateway signs the body
|
|
525
|
+
// hash and sends it as a header), so an unsigned request is refused
|
|
526
|
+
// before its body is read; the body is then read under the cap, hashed
|
|
527
|
+
// as it arrives, and must match the signed hash.
|
|
528
|
+
const headers = context.req.raw.headers;
|
|
529
|
+
const pathWithQuery = `${url.pathname}${url.search}`;
|
|
530
|
+
const verified = verifySiteGatewayRequest({ method, pathWithQuery, headers, secrets });
|
|
531
|
+
if (!verified.ok) throw refuse(verified.reason);
|
|
532
|
+
const suffix = url.pathname.slice(SITE_RUNTIME_API_PREFIX.length);
|
|
533
|
+
const read =
|
|
534
|
+
method === "GET" || method === "HEAD"
|
|
535
|
+
? { body: new Uint8Array(), sha256: EMPTY_BODY_SHA256 }
|
|
536
|
+
: await readCappedBody(
|
|
537
|
+
context.req.raw,
|
|
538
|
+
method === "POST" && suffix === FILE_UPLOAD_SUFFIX
|
|
539
|
+
? SITE_RUNTIME_FILE_BODY_LIMIT
|
|
540
|
+
: SITE_RUNTIME_JSON_BODY_LIMIT,
|
|
541
|
+
);
|
|
542
|
+
if (!sameSha256Hex(read.sha256, verified.bodySha256)) throw refuse("body_mismatch");
|
|
543
|
+
const body = read.body;
|
|
544
|
+
context.header("cache-control", "no-store");
|
|
545
|
+
const runtime: RuntimeRequest = {
|
|
546
|
+
context,
|
|
547
|
+
url,
|
|
548
|
+
method,
|
|
549
|
+
suffix,
|
|
550
|
+
body,
|
|
551
|
+
fields: verified.fields,
|
|
552
|
+
domain,
|
|
553
|
+
};
|
|
554
|
+
return await route(runtime);
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const route = async (request: RuntimeRequest): Promise<Response> => {
|
|
558
|
+
const { method, suffix } = request;
|
|
559
|
+
if (method === "GET" && suffix === "/host") return await hostLookup(request);
|
|
560
|
+
const version = /^\/versions\/([^/]+)\/html$/u.exec(suffix);
|
|
561
|
+
if (method === "GET" && version) return await versionHtml(request, version[1]!);
|
|
562
|
+
if (method === "POST" && suffix === "/auth/exchange") return await exchange(request);
|
|
563
|
+
if (method === "POST" && suffix === "/auth/logout") return await logout(request);
|
|
564
|
+
if (method === "POST" && suffix === "/auth/view-as") return await viewAs(request);
|
|
565
|
+
if (method === "POST" && suffix === "/platform/requests")
|
|
566
|
+
return await platformRequests(request);
|
|
567
|
+
if (method === "POST" && suffix === "/site/reports") return await report(request);
|
|
568
|
+
const tool = /^\/tools\/([a-z]+)$/u.exec(suffix);
|
|
569
|
+
if (method === "POST" && tool) return await tools(request, tool[1]!);
|
|
570
|
+
const sitePath = suffix === "/v1/config/client" ? suffix : `/v1/workspaces/site-host${suffix}`;
|
|
571
|
+
const matched = matchSiteOperation(method, sitePath);
|
|
572
|
+
if (!matched) throw unsupported();
|
|
573
|
+
return await operation(request, matched.operation.id);
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* The host map record for the signed host (the gateway's fallback on a KV
|
|
578
|
+
* miss). The signed host header names it; an optional `host` query must agree.
|
|
579
|
+
*/
|
|
580
|
+
const hostLookup = async (request: RuntimeRequest): Promise<Response> => {
|
|
581
|
+
const asked = request.url.searchParams.get("host");
|
|
582
|
+
const normalize = (host: string) => host.trim().toLowerCase().replace(/\.$/u, "");
|
|
583
|
+
if (asked !== null && normalize(asked) !== normalize(request.fields.host))
|
|
584
|
+
throw invalid("host must be the signed Site host");
|
|
585
|
+
const found = await findSiteRuntimeHost(deps, request.domain, request.fields.host);
|
|
586
|
+
if (!found)
|
|
587
|
+
throw sitePublishingError(404, "not_found", "site_not_found", "This Site does not exist");
|
|
588
|
+
// A lookup reaches the API only on an edge miss: repair a lost edge sync.
|
|
589
|
+
healSiteEdgeIfStale(deps, found.publication);
|
|
590
|
+
return request.context.json(
|
|
591
|
+
siteHostMapRecord(deps.settings, request.domain, found.publication, found.site),
|
|
592
|
+
);
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
/** A viewer whose access is checked, with per-visitor request limits applied. */
|
|
596
|
+
const viewing = async (request: RuntimeRequest) => {
|
|
597
|
+
const host = await requireServedSiteRuntimeHost(deps, request.domain, request.fields.host);
|
|
598
|
+
const viewer = await resolveSiteRuntimeViewer(deps, host, request.fields);
|
|
599
|
+
const { limits, kind: profileKind } = await siteRuntimeLimitProfile(deps, host, viewer);
|
|
600
|
+
await enforceSiteRuntimeViewerRate(
|
|
601
|
+
deps,
|
|
602
|
+
host,
|
|
603
|
+
viewer,
|
|
604
|
+
request.fields,
|
|
605
|
+
"requests",
|
|
606
|
+
limits.requestsPerMinutePerVisitor,
|
|
607
|
+
);
|
|
608
|
+
return { host, viewer, limits, profileKind };
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Immutable HTML of the Site's current version, or of a version a publish
|
|
613
|
+
* or rollback replaced within the last ten minutes (pages already open).
|
|
614
|
+
* Never another Site's version, nor an older one of this Site.
|
|
615
|
+
*/
|
|
616
|
+
const versionHtml = async (request: RuntimeRequest, versionId: string): Promise<Response> => {
|
|
617
|
+
const { host } = await viewing(request);
|
|
618
|
+
if (!deps.objectStorage)
|
|
619
|
+
throw sitePublishingError(
|
|
620
|
+
503,
|
|
621
|
+
"upstream_unavailable",
|
|
622
|
+
"site_storage_unavailable",
|
|
623
|
+
"Storage is not configured",
|
|
624
|
+
);
|
|
625
|
+
const parsed = VersionId.safeParse(versionId);
|
|
626
|
+
const version = parsed.success
|
|
627
|
+
? await getServableSiteVersion(deps.db, host.scope, {
|
|
628
|
+
siteId: host.site.id,
|
|
629
|
+
versionId: parsed.data,
|
|
630
|
+
currentVersionId: host.versionId,
|
|
631
|
+
graceSeconds: SITE_VERSION_GRACE_SECONDS,
|
|
632
|
+
})
|
|
633
|
+
: null;
|
|
634
|
+
const object = version ? await deps.objectStorage.getObjectBytes(version.contentKey) : null;
|
|
635
|
+
if (!version || !object)
|
|
636
|
+
throw sitePublishingError(404, "not_found", "site_version_not_found", "Version not found");
|
|
637
|
+
return new Response(object.bytes as Uint8Array<ArrayBuffer>, {
|
|
638
|
+
headers: {
|
|
639
|
+
...USER_CONTENT_SECURITY_HEADERS,
|
|
640
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
641
|
+
"Content-Security-Policy": SITE_VERSION_CONTENT_SECURITY_POLICY,
|
|
642
|
+
// A version's bytes never change; the gateway caches them after its own access check.
|
|
643
|
+
"Cache-Control": "private, max-age=31536000, immutable",
|
|
644
|
+
"Referrer-Policy": "no-referrer",
|
|
645
|
+
...(version.contentSha256 ? { ETag: `"${version.contentSha256}"` } : {}),
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
/** Viewer sign-in, step two: the gateway trades the one-time code for a site-session. */
|
|
651
|
+
const exchange = async (request: RuntimeRequest): Promise<Response> => {
|
|
652
|
+
const host = await requireServedSiteRuntimeHost(deps, request.domain, request.fields.host);
|
|
653
|
+
const input = parseJson(request.body, SiteAuthExchangeRequest);
|
|
654
|
+
await enforceSiteRuntimeRate(
|
|
655
|
+
deps,
|
|
656
|
+
host,
|
|
657
|
+
siteClientAddressPrincipal(request.fields),
|
|
658
|
+
"sign_in",
|
|
659
|
+
EXCHANGES_PER_MINUTE,
|
|
660
|
+
);
|
|
661
|
+
const refused = () =>
|
|
662
|
+
sitePublishingError(
|
|
663
|
+
400,
|
|
664
|
+
"validation_failed",
|
|
665
|
+
"site_auth_code_invalid",
|
|
666
|
+
"This sign-in link is no longer valid",
|
|
667
|
+
);
|
|
668
|
+
const code = await consumeSiteAuthCode(deps.db, host.scope, {
|
|
669
|
+
siteId: host.site.id,
|
|
670
|
+
codeHash: sha256Text(input.code),
|
|
671
|
+
hostname: host.hostname,
|
|
672
|
+
});
|
|
673
|
+
if (!code) throw refused();
|
|
674
|
+
const callback = `https://${host.hostname}${SITE_GATEWAY_CALLBACK_PATH}`;
|
|
675
|
+
const challenge = createHash("sha256").update(input.codeVerifier, "utf8").digest("base64url");
|
|
676
|
+
if (
|
|
677
|
+
input.redirectUri !== callback ||
|
|
678
|
+
!sameText(code.redirectUri, input.redirectUri) ||
|
|
679
|
+
!sameText(code.codeChallenge, challenge)
|
|
680
|
+
)
|
|
681
|
+
throw refused();
|
|
682
|
+
// Access is decided again now: membership or an invitation may have changed.
|
|
683
|
+
const kind = signedInSiteViewerKind({
|
|
684
|
+
access: effectiveSiteAccess(host.publication.access, deps.settings),
|
|
685
|
+
member: await isSiteWorkspaceMember(deps.db, host.scope, code.subjectId),
|
|
686
|
+
email: code.email,
|
|
687
|
+
invitedEmails: host.publication.invitedEmails,
|
|
688
|
+
});
|
|
689
|
+
if (!kind)
|
|
690
|
+
throw sitePublishingError(
|
|
691
|
+
403,
|
|
692
|
+
"forbidden",
|
|
693
|
+
"site_access_denied",
|
|
694
|
+
"You do not have access to this Site",
|
|
695
|
+
);
|
|
696
|
+
const sessionToken = randomBytes(32).toString("base64url");
|
|
697
|
+
const session = await createSiteViewerSession(deps.db, host.scope, {
|
|
698
|
+
siteId: host.site.id,
|
|
699
|
+
tokenHash: sha256Text(sessionToken),
|
|
700
|
+
subjectId: code.subjectId,
|
|
701
|
+
kind,
|
|
702
|
+
email: code.email,
|
|
703
|
+
displayName: code.displayName,
|
|
704
|
+
absoluteSeconds: deps.settings.sitesViewerSessionAbsoluteSeconds,
|
|
705
|
+
});
|
|
706
|
+
const viewer: SiteRuntimeViewer = {
|
|
707
|
+
kind,
|
|
708
|
+
principal: `user:${code.subjectId}`,
|
|
709
|
+
subjectId: code.subjectId,
|
|
710
|
+
name: code.displayName,
|
|
711
|
+
email: code.email,
|
|
712
|
+
sessionId: session.id,
|
|
713
|
+
};
|
|
714
|
+
await meterSiteRuntime(deps, host, { type: "sign_in", viewer });
|
|
715
|
+
const response: SiteAuthExchangeResponse = {
|
|
716
|
+
sessionToken,
|
|
717
|
+
expiresAt: session.expiresAt.toISOString(),
|
|
718
|
+
viewer: publicSiteViewer(viewer),
|
|
719
|
+
};
|
|
720
|
+
return request.context.json(response);
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
const logout = async (request: RuntimeRequest): Promise<Response> => {
|
|
724
|
+
const host = await findSiteRuntimeHost(deps, request.domain, request.fields.host);
|
|
725
|
+
// Signing out always succeeds for the gateway, even for a Site that is gone.
|
|
726
|
+
if (!host) return request.context.json({ revoked: false });
|
|
727
|
+
const revoked = request.fields.session
|
|
728
|
+
? await revokeSiteViewerSession(deps.db, host.scope, {
|
|
729
|
+
siteId: host.site.id,
|
|
730
|
+
tokenHash: sha256Text(request.fields.session),
|
|
731
|
+
})
|
|
732
|
+
: false;
|
|
733
|
+
return request.context.json({ revoked });
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* "View as visitor": a workspace member starts (`invited`, `public`) or
|
|
738
|
+
* ends (null) previewing this Site as that role on their own site-session.
|
|
739
|
+
* Refused for anyone else; the preview only ever shows less (see
|
|
740
|
+
* resolveSiteRuntimeViewer).
|
|
741
|
+
*/
|
|
742
|
+
const viewAs = async (request: RuntimeRequest): Promise<Response> => {
|
|
743
|
+
const host = await requireServedSiteRuntimeHost(deps, request.domain, request.fields.host);
|
|
744
|
+
const input = parseJson(request.body, z.object({ viewAs: SiteViewAs.nullable() }));
|
|
745
|
+
const viewer = await resolveSiteRuntimeViewer(deps, host, request.fields, {
|
|
746
|
+
ignoreViewAs: true,
|
|
747
|
+
});
|
|
748
|
+
if (viewer.kind !== "member" || !viewer.sessionId)
|
|
749
|
+
throw sitePublishingError(
|
|
750
|
+
403,
|
|
751
|
+
"forbidden",
|
|
752
|
+
"site_view_as_members_only",
|
|
753
|
+
"Only workspace members can preview a Site as a visitor",
|
|
754
|
+
);
|
|
755
|
+
await setSiteViewerSessionViewAs(deps.db, host.scope, {
|
|
756
|
+
siteId: host.site.id,
|
|
757
|
+
sessionId: viewer.sessionId,
|
|
758
|
+
viewAs: input.viewAs,
|
|
759
|
+
});
|
|
760
|
+
return request.context.json({ viewAs: input.viewAs });
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Batched gateway request counts (platform usage): the gateway counts
|
|
765
|
+
* every request it serves for a Site host, cache hits included, and
|
|
766
|
+
* reports them in signed batches on the Sites domain apex. Each host's
|
|
767
|
+
* count is recorded once per report id, so a retried report never counts
|
|
768
|
+
* twice. Hosts that no longer resolve are skipped. Hosts are recorded a few
|
|
769
|
+
* at a time, and each workspace's charges are settled once per report.
|
|
770
|
+
*/
|
|
771
|
+
const platformRequests = async (request: RuntimeRequest): Promise<Response> => {
|
|
772
|
+
const apex = request.domain.toLowerCase();
|
|
773
|
+
if (request.fields.host.toLowerCase().replace(/\.$/u, "") !== apex)
|
|
774
|
+
throw invalid("Platform reports are signed for the Sites domain");
|
|
775
|
+
const input = parseJson(request.body, SitePlatformRequestReport);
|
|
776
|
+
const perHost = new Map<string, number>();
|
|
777
|
+
for (const entry of input.hosts) {
|
|
778
|
+
const name = entry.host.toLowerCase();
|
|
779
|
+
perHost.set(name, (perHost.get(name) ?? 0) + entry.requests);
|
|
780
|
+
}
|
|
781
|
+
const entries = [...perHost];
|
|
782
|
+
const workspaces = new Map<string, SiteRuntimeHost["scope"]>();
|
|
783
|
+
let counted = 0;
|
|
784
|
+
const recordOne = async ([name, requests]: [string, number]) => {
|
|
785
|
+
const host = await findSiteRuntimeHost(deps, request.domain, name);
|
|
786
|
+
if (!host) return;
|
|
787
|
+
const recorded = await recordSitePlatformRequests(deps.db, host.scope, {
|
|
788
|
+
siteId: host.site.id,
|
|
789
|
+
requests,
|
|
790
|
+
idempotencyKey: `site_platform.requests:${input.reportId}:${name}`,
|
|
791
|
+
});
|
|
792
|
+
if (!recorded) return;
|
|
793
|
+
counted += requests;
|
|
794
|
+
workspaces.set(host.scope.workspaceId, host.scope);
|
|
795
|
+
};
|
|
796
|
+
for (let next = 0; next < entries.length; next += SITE_PLATFORM_REPORT_CONCURRENCY)
|
|
797
|
+
await Promise.all(
|
|
798
|
+
entries.slice(next, next + SITE_PLATFORM_REPORT_CONCURRENCY).map(recordOne),
|
|
799
|
+
);
|
|
800
|
+
for (const scope of workspaces.values()) {
|
|
801
|
+
const usage = await readSitePlatformUsage(deps.db, scope);
|
|
802
|
+
if (usage) await afterSitePlatformUsage(deps, scope, usage);
|
|
803
|
+
}
|
|
804
|
+
return request.context.json({ counted });
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Abuse reports: anyone who reaches a served Site host, rate limited per
|
|
809
|
+
* reporter, per client address and per Site. A reporter and a client
|
|
810
|
+
* address each get the same small share (5 a minute by default) of the
|
|
811
|
+
* Site's 60, so no one reporter fills the Site's cap. Each report also
|
|
812
|
+
* reaches the platform (a structured log line, and an email when an abuse
|
|
813
|
+
* address is set).
|
|
814
|
+
*/
|
|
815
|
+
const report = async (request: RuntimeRequest): Promise<Response> => {
|
|
816
|
+
const host = await requireServedSiteRuntimeHost(deps, request.domain, request.fields.host);
|
|
817
|
+
const input = parseJson(request.body, SiteAbuseReportRequest);
|
|
818
|
+
const address = siteClientAddressPrincipal(request.fields);
|
|
819
|
+
const reporter = request.fields.session
|
|
820
|
+
? `session:${sha256Text(request.fields.session).slice(0, 32)}`
|
|
821
|
+
: request.fields.visitor
|
|
822
|
+
? `guest:${request.fields.visitor.slice(0, 128)}`
|
|
823
|
+
: address;
|
|
824
|
+
const perVisitor = deps.settings.sitesReportsPerMinutePerVisitor;
|
|
825
|
+
await enforceSiteRuntimeRate(deps, host, reporter, "reports", perVisitor);
|
|
826
|
+
if (reporter !== address)
|
|
827
|
+
await enforceSiteRuntimeRate(deps, host, address, "reports", perVisitor);
|
|
828
|
+
await enforceSiteRuntimeRate(
|
|
829
|
+
deps,
|
|
830
|
+
host,
|
|
831
|
+
SITE_RUNTIME_SITE_PRINCIPAL,
|
|
832
|
+
"reports",
|
|
833
|
+
SITE_REPORTS_PER_MINUTE,
|
|
834
|
+
);
|
|
835
|
+
const created = await createSiteAbuseReport(deps.db, host.scope, {
|
|
836
|
+
siteId: host.site.id,
|
|
837
|
+
hostname: host.hostname,
|
|
838
|
+
category: input.category,
|
|
839
|
+
message: input.message,
|
|
840
|
+
contactEmail: input.contactEmail,
|
|
841
|
+
reporterPrincipal: reporter,
|
|
842
|
+
});
|
|
843
|
+
await meterSiteRuntime(deps, host, { type: "report" });
|
|
844
|
+
notifySiteAbuse(deps, {
|
|
845
|
+
scope: host.scope,
|
|
846
|
+
siteId: host.site.id,
|
|
847
|
+
hostname: host.hostname,
|
|
848
|
+
kind: "report",
|
|
849
|
+
detail: `${input.category}: ${input.message}`,
|
|
850
|
+
});
|
|
851
|
+
return request.context.json({ id: created.id, received: true }, 201);
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
/** Page tools use the signed-in member, not the Site's assistant or owner. */
|
|
855
|
+
const tools = async (request: RuntimeRequest, method: string): Promise<Response> => {
|
|
856
|
+
if (!(SITE_TOOL_METHODS as readonly string[]).includes(method)) throw unsupported();
|
|
857
|
+
await viewing(request);
|
|
858
|
+
const authorization = await authorizePublishedSiteTools(deps, request.domain, request.fields);
|
|
859
|
+
const authority = publishedSiteToolAuthority(authorization);
|
|
860
|
+
// Host-selected association overwrites any page-supplied Site context.
|
|
861
|
+
const site = {
|
|
862
|
+
siteArtifactId: authority.origin.siteId,
|
|
863
|
+
siteVersionId: authority.origin.versionId,
|
|
864
|
+
};
|
|
865
|
+
const authorizeSite: typeof requireWorkspaceSiteToolAuthorization = (db, grant, context) =>
|
|
866
|
+
requireWorkspaceSiteToolAuthorization(db, grant, context, authority.readVersion);
|
|
867
|
+
const options = { signal: request.context.req.raw.signal };
|
|
868
|
+
let result: unknown;
|
|
869
|
+
if (method === "resolve") {
|
|
870
|
+
result = await resolveWorkspaceToolTarget(
|
|
871
|
+
deps,
|
|
872
|
+
authorization,
|
|
873
|
+
{ ...parseJson(request.body, ToolGatewayResolveRequest), ...site },
|
|
874
|
+
options,
|
|
875
|
+
);
|
|
876
|
+
} else if (method === "invoke") {
|
|
877
|
+
result = await withSiteSecretContext(
|
|
878
|
+
{ siteId: site.siteArtifactId, siteVersion: site.siteVersionId },
|
|
879
|
+
() =>
|
|
880
|
+
invokeWorkspaceToolTarget(
|
|
881
|
+
deps,
|
|
882
|
+
authorization,
|
|
883
|
+
{ ...parseJson(request.body, ToolGatewayInvokeRequest), ...site },
|
|
884
|
+
options,
|
|
885
|
+
),
|
|
886
|
+
);
|
|
887
|
+
} else {
|
|
888
|
+
const call =
|
|
889
|
+
method === "call" ? { ...parseJson(request.body, ToolGatewayCallRequest), ...site } : null;
|
|
890
|
+
// Reject undeclared identities before any connector is prepared.
|
|
891
|
+
if (call)
|
|
892
|
+
await authorizeSite(deps.db, authorization.grant, { ...site, identity: call.identity });
|
|
893
|
+
const prepared = await prepareWorkspaceToolGateway(deps, authorization, options);
|
|
894
|
+
try {
|
|
895
|
+
result = call
|
|
896
|
+
? await callWorkspaceToolGateway(
|
|
897
|
+
prepared,
|
|
898
|
+
authorization.grant,
|
|
899
|
+
call,
|
|
900
|
+
deps.db,
|
|
901
|
+
undefined,
|
|
902
|
+
deps.observability,
|
|
903
|
+
authorizeSite,
|
|
904
|
+
async () => authority.origin,
|
|
905
|
+
)
|
|
906
|
+
: method === "declarations"
|
|
907
|
+
? workspaceToolGatewayDeclarations(prepared)
|
|
908
|
+
: prepared.toolGatewayCatalog;
|
|
909
|
+
if (!call) await authority.reauthorize();
|
|
910
|
+
} finally {
|
|
911
|
+
await prepared.close();
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return Response.json(result, { headers: { "cache-control": "no-store" } });
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* The models a viewer's conversations may use. Members: the Site agent's
|
|
919
|
+
* model and the allowed models, subscriptions included. Everyone else
|
|
920
|
+
* never uses a subscription-billed model: a subscription agent model is
|
|
921
|
+
* replaced by the workspace's default credit-billed model, and
|
|
922
|
+
* `unavailable` when there is none (agent-model.ts).
|
|
923
|
+
*/
|
|
924
|
+
const siteViewerModels = async (
|
|
925
|
+
host: SiteRuntimeHost,
|
|
926
|
+
viewer: SiteRuntimeViewer,
|
|
927
|
+
limits: { agentModels: string[] | null },
|
|
928
|
+
): Promise<{ allowed: string[]; default: string | null; unavailable: boolean }> => {
|
|
929
|
+
const agentModel = host.publication.agent.model;
|
|
930
|
+
const named = (models: (string | null | undefined)[]) => [
|
|
931
|
+
...new Set(
|
|
932
|
+
models.filter((model): model is string => typeof model === "string" && model.length > 0),
|
|
933
|
+
),
|
|
934
|
+
];
|
|
935
|
+
if (viewer.kind === "member") {
|
|
936
|
+
if (!viewer.subjectId)
|
|
937
|
+
throw siteNotAvailableForViewer("agent_model", "Sign in to use your workspace's models");
|
|
938
|
+
const selections = resolveWorkspaceModelSelection(
|
|
939
|
+
await loadWorkspaceModelSelectionInput(deps.db, deps.settings, {
|
|
940
|
+
...host.scope,
|
|
941
|
+
subjectId: viewer.subjectId,
|
|
942
|
+
}),
|
|
943
|
+
);
|
|
944
|
+
return {
|
|
945
|
+
allowed: selections
|
|
946
|
+
.filter((selection) => admissibleWorkspaceModel([selection], selection.model.id))
|
|
947
|
+
.map((selection) => selection.model.id),
|
|
948
|
+
default: agentModel,
|
|
949
|
+
unavailable: false,
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
const choice = await resolveSiteOutsideModel(deps, host.scope, {
|
|
953
|
+
siteId: host.site.id,
|
|
954
|
+
agentModel,
|
|
955
|
+
});
|
|
956
|
+
const creditModels = new Set(await siteCreditModelIds(deps, host.scope, host.site.id));
|
|
957
|
+
const credited = (models: string[]) => models.filter((model) => creditModels.has(model));
|
|
958
|
+
if (choice.kind === "unavailable")
|
|
959
|
+
return {
|
|
960
|
+
allowed: credited(named(limits.agentModels ?? [])),
|
|
961
|
+
default: null,
|
|
962
|
+
unavailable: true,
|
|
963
|
+
};
|
|
964
|
+
const model = choice.model;
|
|
965
|
+
return {
|
|
966
|
+
allowed: credited(named([model, ...(limits.agentModels ?? [])])),
|
|
967
|
+
default: model,
|
|
968
|
+
unavailable: false,
|
|
969
|
+
};
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* What this viewer can use, from the same checks the routes enforce: the
|
|
974
|
+
* agent also needs the platform brake open for people outside the
|
|
975
|
+
* workspace who are not invited, and a credit-billed model for anyone
|
|
976
|
+
* outside the workspace. Page tools are independent of the assistant.
|
|
977
|
+
*/
|
|
978
|
+
const viewerCapabilities = async (
|
|
979
|
+
host: SiteRuntimeHost,
|
|
980
|
+
viewer: SiteRuntimeViewer,
|
|
981
|
+
limits: Parameters<typeof siteViewerCapabilities>[3],
|
|
982
|
+
) => {
|
|
983
|
+
const base = siteViewerCapabilities(deps, host, viewer, limits);
|
|
984
|
+
let agent = base.agent;
|
|
985
|
+
if (agent && (viewer.kind === "guest" || viewer.kind === "user"))
|
|
986
|
+
agent = !(await sitePlatformBrakeEngaged(deps).catch(() => false));
|
|
987
|
+
if (agent && viewer.kind !== "member") {
|
|
988
|
+
const choice = await resolveSiteOutsideModel(deps, host.scope, {
|
|
989
|
+
siteId: host.site.id,
|
|
990
|
+
agentModel: host.publication.agent.model,
|
|
991
|
+
}).catch(() => null);
|
|
992
|
+
agent = choice !== null && choice.kind !== "unavailable";
|
|
993
|
+
}
|
|
994
|
+
const version = host.site.currentVersionId
|
|
995
|
+
? await getPublishedSiteVersion(deps.db, host.scope, host.site.id, host.site.currentVersionId)
|
|
996
|
+
: null;
|
|
997
|
+
const workspaceTools =
|
|
998
|
+
base.workspaceTools &&
|
|
999
|
+
(version?.requestedTools.length ?? 0) > 0 &&
|
|
1000
|
+
viewer.subjectId !== null &&
|
|
1001
|
+
viewer.subjectId !== undefined &&
|
|
1002
|
+
hasPermission(
|
|
1003
|
+
(await getSiteWorkspaceMemberPermissions(deps.db, host.scope, viewer.subjectId)) ?? [],
|
|
1004
|
+
"workspace:read",
|
|
1005
|
+
);
|
|
1006
|
+
return { ...base, agent, workspaceTools };
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
const operation = async (request: RuntimeRequest, id: string): Promise<Response> => {
|
|
1010
|
+
const { host, viewer, limits, profileKind } = await viewing(request);
|
|
1011
|
+
const { context } = request;
|
|
1012
|
+
switch (id) {
|
|
1013
|
+
case "config.client":
|
|
1014
|
+
return siteRuntimeForwardedResponse(
|
|
1015
|
+
await dispatch(new Request(new URL("/v1/config/client", request.url.origin))),
|
|
1016
|
+
);
|
|
1017
|
+
case "workspace.get":
|
|
1018
|
+
// The Site's own view of "its workspace": no tenant data.
|
|
1019
|
+
return context.json({ id: host.scope.workspaceId, name: host.site.title });
|
|
1020
|
+
case "site.config": {
|
|
1021
|
+
const capabilities = await viewerCapabilities(host, viewer, limits);
|
|
1022
|
+
const config: SiteRuntimeConfig = {
|
|
1023
|
+
siteId: host.site.id,
|
|
1024
|
+
title: host.site.title,
|
|
1025
|
+
hostname: host.hostname,
|
|
1026
|
+
versionId: host.versionId,
|
|
1027
|
+
access: effectiveSiteAccess(host.publication.access, deps.settings),
|
|
1028
|
+
viewer: publicSiteViewer(viewer),
|
|
1029
|
+
capabilities,
|
|
1030
|
+
agent: {
|
|
1031
|
+
enabled: capabilities.agent,
|
|
1032
|
+
model: host.publication.agent.model,
|
|
1033
|
+
},
|
|
1034
|
+
siteData: true,
|
|
1035
|
+
noindex: host.publication.noindex || deps.settings.sitesForceNoindex,
|
|
1036
|
+
abuseEmail: deps.settings.sitesAbuseEmail ?? `abuse@${request.domain}`,
|
|
1037
|
+
};
|
|
1038
|
+
return context.json(config);
|
|
1039
|
+
}
|
|
1040
|
+
case "models.list": {
|
|
1041
|
+
const models = await siteViewerModels(host, viewer, limits);
|
|
1042
|
+
return context.json({
|
|
1043
|
+
models: models.allowed.map((model) => ({ id: model })),
|
|
1044
|
+
defaultModel: models.default,
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
case "site.report":
|
|
1048
|
+
return await report(request);
|
|
1049
|
+
case "data.read":
|
|
1050
|
+
case "data.write":
|
|
1051
|
+
case "data.put":
|
|
1052
|
+
case "data.patch":
|
|
1053
|
+
case "data.delete":
|
|
1054
|
+
return await siteData(request, host, viewer, limits.guestWritesPerMinute);
|
|
1055
|
+
case "sql.run":
|
|
1056
|
+
return await siteSql(request, host, viewer, limits.guestWritesPerMinute, profileKind);
|
|
1057
|
+
default:
|
|
1058
|
+
if (id.startsWith("sessions.")) return await sessions(request, id, host, viewer, limits);
|
|
1059
|
+
throw unsupported();
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
/** This Site's viewer (`user:<subject>` or `guest:<visitor>`) as Site Data and Site SQL see it. */
|
|
1064
|
+
const publishedViewer = (
|
|
1065
|
+
host: SiteRuntimeHost & { versionId: string },
|
|
1066
|
+
viewer: SiteRuntimeViewer,
|
|
1067
|
+
): PublishedSiteDataViewer =>
|
|
1068
|
+
sitePublishedDataViewer(host, viewer.principal, viewer.kind === "guest");
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Site SQL: run one named query of this Site as the viewer. The query's
|
|
1072
|
+
* audience decides who may run it (guests only `everyone`), and `:viewer`
|
|
1073
|
+
* is bound from this viewer; the Site never sends SQL. Guests' write
|
|
1074
|
+
* queries also count against the guest write limit per visitor and per
|
|
1075
|
+
* client address, like Site Data.
|
|
1076
|
+
*/
|
|
1077
|
+
const siteSql = async (
|
|
1078
|
+
request: RuntimeRequest,
|
|
1079
|
+
host: SiteRuntimeHost & { versionId: string },
|
|
1080
|
+
viewer: SiteRuntimeViewer,
|
|
1081
|
+
guestWritesPerMinute: number | null,
|
|
1082
|
+
profileKind: SiteLimitProfileKind,
|
|
1083
|
+
): Promise<Response> => {
|
|
1084
|
+
const name = /^\/published-artifacts\/current\/sql\/queries\/([^/]+)$/u.exec(
|
|
1085
|
+
request.suffix,
|
|
1086
|
+
)?.[1];
|
|
1087
|
+
if (request.method !== "POST" || !name) throw unsupported();
|
|
1088
|
+
const input = parseJson(request.body, SiteSqlRunRequest);
|
|
1089
|
+
const siteId = host.site.id;
|
|
1090
|
+
return request.context.json(
|
|
1091
|
+
await runSiteSqlRoute(() =>
|
|
1092
|
+
runSiteSqlForViewer(() =>
|
|
1093
|
+
runSiteSqlQuery(
|
|
1094
|
+
deps,
|
|
1095
|
+
publishedViewer(host, viewer),
|
|
1096
|
+
siteId,
|
|
1097
|
+
name,
|
|
1098
|
+
input.params ?? {},
|
|
1099
|
+
{ frameSiteId: siteId },
|
|
1100
|
+
{
|
|
1101
|
+
profileKind,
|
|
1102
|
+
beforeWrite: async () => {
|
|
1103
|
+
if (viewer.kind === "guest")
|
|
1104
|
+
await enforceSiteRuntimeViewerRate(
|
|
1105
|
+
deps,
|
|
1106
|
+
host,
|
|
1107
|
+
viewer,
|
|
1108
|
+
request.fields,
|
|
1109
|
+
"guest_writes",
|
|
1110
|
+
guestWritesPerMinute,
|
|
1111
|
+
);
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
),
|
|
1115
|
+
),
|
|
1116
|
+
),
|
|
1117
|
+
);
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
/** Site Data and files of this Site, as the viewer (`user:<subject>` or `guest:<visitor>`). */
|
|
1121
|
+
const siteData = async (
|
|
1122
|
+
request: RuntimeRequest,
|
|
1123
|
+
host: SiteRuntimeHost & { versionId: string },
|
|
1124
|
+
viewer: SiteRuntimeViewer,
|
|
1125
|
+
guestWritesPerMinute: number | null,
|
|
1126
|
+
): Promise<Response> => {
|
|
1127
|
+
const { context, method, url } = request;
|
|
1128
|
+
const path = request.suffix.slice("/published-artifacts/current".length);
|
|
1129
|
+
const authority = publishedViewer(host, viewer);
|
|
1130
|
+
const siteId = host.site.id;
|
|
1131
|
+
const surface = { frameSiteId: siteId };
|
|
1132
|
+
// Guest writes count per visitor and per client address; both must pass.
|
|
1133
|
+
const guestWrite = async () => {
|
|
1134
|
+
if (viewer.kind === "guest")
|
|
1135
|
+
await enforceSiteRuntimeViewerRate(
|
|
1136
|
+
deps,
|
|
1137
|
+
host,
|
|
1138
|
+
viewer,
|
|
1139
|
+
request.fields,
|
|
1140
|
+
"guest_writes",
|
|
1141
|
+
guestWritesPerMinute,
|
|
1142
|
+
);
|
|
1143
|
+
};
|
|
1144
|
+
const file = /^\/files\/([^/]+)$/u.exec(path);
|
|
1145
|
+
if (method === "POST" && path === "/data/query")
|
|
1146
|
+
return context.json(
|
|
1147
|
+
await runSiteData(() =>
|
|
1148
|
+
querySiteData(
|
|
1149
|
+
deps,
|
|
1150
|
+
authority,
|
|
1151
|
+
siteId,
|
|
1152
|
+
parseJson(request.body, SiteDataQueryRequest),
|
|
1153
|
+
surface,
|
|
1154
|
+
),
|
|
1155
|
+
),
|
|
1156
|
+
);
|
|
1157
|
+
if (method === "POST" && path === "/data/get") {
|
|
1158
|
+
const record = await runSiteData(() =>
|
|
1159
|
+
getSiteDataRecord(
|
|
1160
|
+
deps,
|
|
1161
|
+
authority,
|
|
1162
|
+
siteId,
|
|
1163
|
+
parseJson(request.body, SiteDataGetRequest),
|
|
1164
|
+
surface,
|
|
1165
|
+
),
|
|
1166
|
+
);
|
|
1167
|
+
return context.json({ record });
|
|
1168
|
+
}
|
|
1169
|
+
if (method === "POST" && path === "/data/write") {
|
|
1170
|
+
const input = parseJson(request.body, SiteDataWriteRequest);
|
|
1171
|
+
await guestWrite();
|
|
1172
|
+
const written = await runSiteData(() =>
|
|
1173
|
+
writeSiteData(deps, authority, siteId, input.operations, surface),
|
|
1174
|
+
);
|
|
1175
|
+
return context.json({ results: written.results });
|
|
1176
|
+
}
|
|
1177
|
+
if (method === "GET" && path === "/files") {
|
|
1178
|
+
const limit = Number(url.searchParams.get("limit") ?? "100");
|
|
1179
|
+
return context.json(
|
|
1180
|
+
await runSiteData(() =>
|
|
1181
|
+
listSiteDataFiles(
|
|
1182
|
+
deps,
|
|
1183
|
+
authority,
|
|
1184
|
+
siteId,
|
|
1185
|
+
{ limit: Number.isFinite(limit) ? limit : 100, cursor: url.searchParams.get("cursor") },
|
|
1186
|
+
surface,
|
|
1187
|
+
),
|
|
1188
|
+
),
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
if (method === "POST" && path === "/files") {
|
|
1192
|
+
const scope = SiteDataFileScope.safeParse(url.searchParams.get("scope") ?? "shared");
|
|
1193
|
+
if (!scope.success) throw invalid("scope must be shared or private");
|
|
1194
|
+
await guestWrite();
|
|
1195
|
+
const bytes = request.body;
|
|
1196
|
+
const created = await runSiteData(() =>
|
|
1197
|
+
uploadSiteDataFile(
|
|
1198
|
+
deps,
|
|
1199
|
+
authority,
|
|
1200
|
+
siteId,
|
|
1201
|
+
{
|
|
1202
|
+
name: url.searchParams.get("name"),
|
|
1203
|
+
contentType: context.req.header("content-type"),
|
|
1204
|
+
body: {
|
|
1205
|
+
declaredBytes: bytes.byteLength,
|
|
1206
|
+
read: async () => {
|
|
1207
|
+
if (bytes.byteLength > deps.settings.siteDataMaxFileBytes)
|
|
1208
|
+
throw new SiteDataError("quota_exceeded", "This file is too large", {
|
|
1209
|
+
quota: "file_bytes",
|
|
1210
|
+
limit: deps.settings.siteDataMaxFileBytes,
|
|
1211
|
+
});
|
|
1212
|
+
return bytes;
|
|
1213
|
+
},
|
|
1214
|
+
},
|
|
1215
|
+
scope: scope.data,
|
|
1216
|
+
},
|
|
1217
|
+
surface,
|
|
1218
|
+
),
|
|
1219
|
+
);
|
|
1220
|
+
return context.json(created, 201);
|
|
1221
|
+
}
|
|
1222
|
+
if (method === "GET" && file)
|
|
1223
|
+
return context.json(
|
|
1224
|
+
await runSiteData(() => getSiteDataFileUrl(deps, authority, siteId, file[1]!, surface)),
|
|
1225
|
+
);
|
|
1226
|
+
if (method === "DELETE" && file) {
|
|
1227
|
+
await guestWrite();
|
|
1228
|
+
return context.json(
|
|
1229
|
+
await runSiteData(() => deleteSiteDataFile(deps, authority, siteId, file[1]!, surface)),
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
throw unsupported();
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
/** The viewer's own agent conversations on this Site, run as `site:<siteId>`. */
|
|
1236
|
+
const sessions = async (
|
|
1237
|
+
request: RuntimeRequest,
|
|
1238
|
+
id: string,
|
|
1239
|
+
host: SiteRuntimeHost & { versionId: string },
|
|
1240
|
+
viewer: SiteRuntimeViewer,
|
|
1241
|
+
limits: Awaited<ReturnType<typeof siteRuntimeLimitProfile>>["limits"],
|
|
1242
|
+
): Promise<Response> => {
|
|
1243
|
+
const { context, url } = request;
|
|
1244
|
+
const base = `/v1/workspaces/${host.scope.workspaceId}/sessions`;
|
|
1245
|
+
// Every forwarded call runs inside the Site's origin, and also
|
|
1246
|
+
// carries the Site and version headers the API's Site frame guard
|
|
1247
|
+
// validates; sessions and turns it starts record this Site and version.
|
|
1248
|
+
const siteOrigin = { siteId: host.site.id, title: host.site.title, versionId: host.versionId };
|
|
1249
|
+
let pendingAdmission: { sessionId: string; admissionId: string } | undefined;
|
|
1250
|
+
const send = async (
|
|
1251
|
+
method: string,
|
|
1252
|
+
path: string,
|
|
1253
|
+
body?: unknown,
|
|
1254
|
+
extraPermissions?: readonly Permission[],
|
|
1255
|
+
) => {
|
|
1256
|
+
const execute = () =>
|
|
1257
|
+
withSiteSessionOrigin(siteOrigin, async () =>
|
|
1258
|
+
dispatch(
|
|
1259
|
+
await siteRuntimeSessionRequest(deps, host, viewer, {
|
|
1260
|
+
origin: url.origin,
|
|
1261
|
+
method,
|
|
1262
|
+
path,
|
|
1263
|
+
...(body === undefined ? {} : { body }),
|
|
1264
|
+
...(extraPermissions ? { extraPermissions } : {}),
|
|
1265
|
+
}),
|
|
1266
|
+
),
|
|
1267
|
+
);
|
|
1268
|
+
const pending = pendingAdmission;
|
|
1269
|
+
if (!pending) return await execute();
|
|
1270
|
+
// The trusted context fences every session write through enqueue commit.
|
|
1271
|
+
// A thrown/unknown dispatch retains its marker; recovery reconciles it.
|
|
1272
|
+
const response = await withSiteRuntimeAdmission(
|
|
1273
|
+
{ ...pending, workspaceId: host.scope.workspaceId },
|
|
1274
|
+
execute,
|
|
1275
|
+
);
|
|
1276
|
+
await finishSiteRuntimeAdmission(deps.db, host.scope, pending);
|
|
1277
|
+
pendingAdmission = undefined;
|
|
1278
|
+
if (!response.ok) {
|
|
1279
|
+
const current = await getOwnedSiteRuntimeConversation(deps.db, host.scope, {
|
|
1280
|
+
sessionId: pending.sessionId,
|
|
1281
|
+
siteId: host.site.id,
|
|
1282
|
+
viewerPrincipal: viewer.principal,
|
|
1283
|
+
});
|
|
1284
|
+
if (current) await settleSiteRuntimeSpend(deps, host, current, { release: true });
|
|
1285
|
+
}
|
|
1286
|
+
return response;
|
|
1287
|
+
};
|
|
1288
|
+
/** A forwarded session read, answered with the Site-safe projection. */
|
|
1289
|
+
const projected = async (response: Response, status = response.status) => {
|
|
1290
|
+
if (!response.ok) return siteRuntimeForwardedResponse(response);
|
|
1291
|
+
return context.json(siteSessionProjection(await response.json()), status as 200);
|
|
1292
|
+
};
|
|
1293
|
+
/** Forward, and answer a success with a Site-safe view of its body. */
|
|
1294
|
+
const forwardView = async (
|
|
1295
|
+
method: string,
|
|
1296
|
+
path: string,
|
|
1297
|
+
view: (value: unknown) => unknown,
|
|
1298
|
+
body?: unknown,
|
|
1299
|
+
) => {
|
|
1300
|
+
const response = await send(method, path, body);
|
|
1301
|
+
if (!response.ok) return siteRuntimeForwardedResponse(response);
|
|
1302
|
+
return context.json(view(await response.json()) as object, response.status as 200);
|
|
1303
|
+
};
|
|
1304
|
+
const notFound = () =>
|
|
1305
|
+
sitePublishingError(
|
|
1306
|
+
404,
|
|
1307
|
+
"not_found",
|
|
1308
|
+
"site_conversation_not_found",
|
|
1309
|
+
"Conversation not found",
|
|
1310
|
+
);
|
|
1311
|
+
const budgets = siteRuntimeBudgetLimits(limits, deps.settings);
|
|
1312
|
+
/**
|
|
1313
|
+
* Reserve, and when a Site-wide budget refuses, release reservations
|
|
1314
|
+
* finished conversations no longer need and retry once (unless the Site
|
|
1315
|
+
* was swept moments ago).
|
|
1316
|
+
*/
|
|
1317
|
+
const reserve = async (run: () => Promise<SiteRuntimeReservation>) => {
|
|
1318
|
+
let reservation = await run();
|
|
1319
|
+
if (siteWideBudget(reservation) && (await sweepSiteRuntimeReservations(deps, host)))
|
|
1320
|
+
reservation = await run();
|
|
1321
|
+
if (!reservation.ok) throw siteReservationRefused(reservation);
|
|
1322
|
+
return reservation;
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
if (id === "sessions.list") {
|
|
1326
|
+
const owned = await listOwnedSiteRuntimeConversations(deps.db, host.scope, {
|
|
1327
|
+
siteId: host.site.id,
|
|
1328
|
+
viewerPrincipal: viewer.principal,
|
|
1329
|
+
limit: LIST_LIMIT,
|
|
1330
|
+
});
|
|
1331
|
+
const listed: unknown[] = [];
|
|
1332
|
+
for (const conversation of owned) {
|
|
1333
|
+
const response = await send("GET", `${base}/${conversation.sessionId}`);
|
|
1334
|
+
if (response.ok) listed.push(siteSessionProjection(await response.json()));
|
|
1335
|
+
else await response.body?.cancel();
|
|
1336
|
+
}
|
|
1337
|
+
return context.json({ pinned: [], sessions: listed, nextCursor: null });
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
if (id === "sessions.create") {
|
|
1341
|
+
const input = parseJson(request.body, CreateConversationRequest);
|
|
1342
|
+
requireSiteAgent(deps, host, viewer, limits);
|
|
1343
|
+
// The Opengeni-wide platform brake stops public visitors' agent turns.
|
|
1344
|
+
if (viewer.kind === "guest" || viewer.kind === "user")
|
|
1345
|
+
await requireSitePlatformOpen(deps, "guest_agent");
|
|
1346
|
+
const models = await siteViewerModels(host, viewer, limits);
|
|
1347
|
+
if (models.unavailable)
|
|
1348
|
+
throw siteNotAvailableForViewer(
|
|
1349
|
+
"agent_model",
|
|
1350
|
+
"This Site's assistant is only available to workspace members right now",
|
|
1351
|
+
{ reason: "no_credit_billed_model" },
|
|
1352
|
+
);
|
|
1353
|
+
if (
|
|
1354
|
+
viewer.kind === "guest" &&
|
|
1355
|
+
deps.settings.sitesGuestAgentHumanCheck &&
|
|
1356
|
+
!request.fields.human
|
|
1357
|
+
)
|
|
1358
|
+
throw sitePublishingError(
|
|
1359
|
+
403,
|
|
1360
|
+
"forbidden",
|
|
1361
|
+
"site_human_check_required",
|
|
1362
|
+
"Confirm you are human to use this Site's assistant",
|
|
1363
|
+
);
|
|
1364
|
+
// A replayed idempotency key names the same conversation: answer it again.
|
|
1365
|
+
const requestedSessionId = input.idempotencyKey
|
|
1366
|
+
? idempotentSessionId(`${host.site.id}\n${viewer.principal}\n${input.idempotencyKey}`)
|
|
1367
|
+
: randomUUID();
|
|
1368
|
+
let repair: { conversation: SiteRuntimeConversationRecord; model: string } | undefined;
|
|
1369
|
+
if (input.idempotencyKey) {
|
|
1370
|
+
const replay = await getOwnedSiteRuntimeConversation(deps.db, host.scope, {
|
|
1371
|
+
sessionId: requestedSessionId,
|
|
1372
|
+
siteId: host.site.id,
|
|
1373
|
+
viewerPrincipal: viewer.principal,
|
|
1374
|
+
});
|
|
1375
|
+
if (replay) {
|
|
1376
|
+
// A lost start response may have left only a reservation. Reconcile
|
|
1377
|
+
// before replay so this key cannot remain stuck on a missing session.
|
|
1378
|
+
await settleSiteRuntimeSpend(deps, host, replay);
|
|
1379
|
+
const retained = await getOwnedSiteRuntimeConversation(deps.db, host.scope, {
|
|
1380
|
+
sessionId: requestedSessionId,
|
|
1381
|
+
siteId: host.site.id,
|
|
1382
|
+
viewerPrincipal: viewer.principal,
|
|
1383
|
+
});
|
|
1384
|
+
if (retained) {
|
|
1385
|
+
const shell = await getSession(deps.db, host.scope.workspaceId, requestedSessionId);
|
|
1386
|
+
const started = await listSessionEvents(
|
|
1387
|
+
deps.db,
|
|
1388
|
+
host.scope.workspaceId,
|
|
1389
|
+
requestedSessionId,
|
|
1390
|
+
{
|
|
1391
|
+
includeTypes: ["session.created"],
|
|
1392
|
+
payloadMode: "none",
|
|
1393
|
+
limit: 1,
|
|
1394
|
+
},
|
|
1395
|
+
);
|
|
1396
|
+
if (!shell || started.length > 0)
|
|
1397
|
+
return await projected(await send("GET", `${base}/${replay.sessionId}`), 202);
|
|
1398
|
+
// Core deliberately retains a keyed shell when initialization fails.
|
|
1399
|
+
// Re-enter its keyed Start repair, never insert/count a new conversation.
|
|
1400
|
+
repair = { conversation: retained, model: shell.model };
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
// Settle the viewer's recent spend so the day budget sees it.
|
|
1405
|
+
for (const conversation of await listOwnedSiteRuntimeConversations(deps.db, host.scope, {
|
|
1406
|
+
siteId: host.site.id,
|
|
1407
|
+
viewerPrincipal: viewer.principal,
|
|
1408
|
+
limit: 5,
|
|
1409
|
+
}))
|
|
1410
|
+
await settleSiteRuntimeSpend(deps, host, conversation);
|
|
1411
|
+
const agent = host.publication.agent;
|
|
1412
|
+
const memberPermissions =
|
|
1413
|
+
viewer.kind === "member" && viewer.subjectId
|
|
1414
|
+
? await getSiteWorkspaceMemberPermissions(deps.db, host.scope, viewer.subjectId)
|
|
1415
|
+
: null;
|
|
1416
|
+
const mcpTools = siteAgentToolRefs(agent, viewer, memberPermissions);
|
|
1417
|
+
if (repair) {
|
|
1418
|
+
const previous = repair.conversation;
|
|
1419
|
+
const legacyMember =
|
|
1420
|
+
viewer.kind === "member" &&
|
|
1421
|
+
previous.viewerKind === "member" &&
|
|
1422
|
+
previous.budgetKey === SITE_RUNTIME_SITE_PRINCIPAL;
|
|
1423
|
+
if (
|
|
1424
|
+
(previous.budgetKey !== siteRuntimeBudgetKey(viewer) && !legacyMember) ||
|
|
1425
|
+
previous.toolIds.some((tool) => !mcpTools.some((allowed) => allowed.id === tool))
|
|
1426
|
+
)
|
|
1427
|
+
throw sitePublishingError(
|
|
1428
|
+
409,
|
|
1429
|
+
"conflict",
|
|
1430
|
+
"site_conversation_outdated",
|
|
1431
|
+
"This Site's assistant changed since this conversation started; start a new one",
|
|
1432
|
+
);
|
|
1433
|
+
if (
|
|
1434
|
+
viewer.kind !== "member" &&
|
|
1435
|
+
!(await siteCreditModelIds(deps, host.scope, host.site.id)).includes(repair.model)
|
|
1436
|
+
)
|
|
1437
|
+
throw siteNotAvailableForViewer(
|
|
1438
|
+
"agent_model",
|
|
1439
|
+
"This Site's assistant cannot use that model for you",
|
|
1440
|
+
{ reason: "subscription_model" },
|
|
1441
|
+
);
|
|
1442
|
+
if (viewer.kind === "member" && previous.limitMicros !== null)
|
|
1443
|
+
await liftSiteConversationUsageLimit(
|
|
1444
|
+
deps,
|
|
1445
|
+
host,
|
|
1446
|
+
requestedSessionId,
|
|
1447
|
+
previous.limitMicros,
|
|
1448
|
+
null,
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
// Every conversation gets this Site's own data tools, pinned to this
|
|
1452
|
+
// Site and acting as this viewer (site-publishing/agent-data-tools.ts).
|
|
1453
|
+
const dataTools = await siteAgentDataToolsForStart(
|
|
1454
|
+
deps,
|
|
1455
|
+
host.scope,
|
|
1456
|
+
host.site.id,
|
|
1457
|
+
viewer.kind,
|
|
1458
|
+
);
|
|
1459
|
+
// One transaction counts the start and reserves a per-turn estimate
|
|
1460
|
+
// against the visitor's and the Site's windows; only the funded allowance
|
|
1461
|
+
// becomes the session's usage limit, set in the create transaction. The
|
|
1462
|
+
// row also records the tools the conversation gets, so narrowing them
|
|
1463
|
+
// later stops it.
|
|
1464
|
+
const admissionId = randomUUID();
|
|
1465
|
+
const reservation = await reserve(() =>
|
|
1466
|
+
repair
|
|
1467
|
+
? reserveSiteRuntimeConversationWork(deps.db, host.scope, {
|
|
1468
|
+
admissionId,
|
|
1469
|
+
sessionId: requestedSessionId,
|
|
1470
|
+
limits: budgets,
|
|
1471
|
+
newTurn: false,
|
|
1472
|
+
uncapped: viewer.kind === "member",
|
|
1473
|
+
repairStart: true,
|
|
1474
|
+
})
|
|
1475
|
+
: reserveSiteRuntimeConversationStart(deps.db, host.scope, {
|
|
1476
|
+
admissionId,
|
|
1477
|
+
sessionId: requestedSessionId,
|
|
1478
|
+
siteId: host.site.id,
|
|
1479
|
+
viewerPrincipal: viewer.principal,
|
|
1480
|
+
viewerKind: viewer.kind,
|
|
1481
|
+
viewerEmail: viewer.email,
|
|
1482
|
+
toolIds: mcpTools.map((tool) => tool.id),
|
|
1483
|
+
versionId: host.versionId,
|
|
1484
|
+
limits: budgets,
|
|
1485
|
+
budgetKey: siteRuntimeBudgetKey(viewer),
|
|
1486
|
+
}),
|
|
1487
|
+
);
|
|
1488
|
+
if (reservation.alreadyStarted)
|
|
1489
|
+
return await projected(await send("GET", `${base}/${requestedSessionId}`), 202);
|
|
1490
|
+
pendingAdmission = { sessionId: requestedSessionId, admissionId };
|
|
1491
|
+
const body = {
|
|
1492
|
+
initialMessage: input.initialMessage,
|
|
1493
|
+
requestedSessionId,
|
|
1494
|
+
startMode: "standard",
|
|
1495
|
+
agentAccess: "session",
|
|
1496
|
+
memoryScope: "off",
|
|
1497
|
+
rigId: null,
|
|
1498
|
+
// Only this Site's data tools; never management or workspace tools.
|
|
1499
|
+
firstPartyMcpTools: dataTools,
|
|
1500
|
+
...(dataTools.length > 0
|
|
1501
|
+
? { firstPartyMcpPermissions: [...SITE_AGENT_DATA_TOOL_PERMISSIONS] }
|
|
1502
|
+
: {}),
|
|
1503
|
+
agent: {
|
|
1504
|
+
capabilities: {
|
|
1505
|
+
from: "none",
|
|
1506
|
+
humanInput: false,
|
|
1507
|
+
skills: false,
|
|
1508
|
+
...(dataTools.length > 0 ? { artifacts: true } : {}),
|
|
1509
|
+
},
|
|
1510
|
+
},
|
|
1511
|
+
// No sandbox: a Site's agent answers from the page and the Site's
|
|
1512
|
+
// data, with no shell or workspace files.
|
|
1513
|
+
sandboxBackend: "none",
|
|
1514
|
+
...(repair ? { model: repair.model } : models.default ? { model: models.default } : {}),
|
|
1515
|
+
instructions: siteAgentInstructions(host, viewer, dataTools, agent.instructions),
|
|
1516
|
+
// Always explicit: an empty list means no workspace tools at all.
|
|
1517
|
+
tools: mcpTools,
|
|
1518
|
+
metadata: {
|
|
1519
|
+
opengeniPublishedSite: {
|
|
1520
|
+
siteId: host.site.id,
|
|
1521
|
+
versionId: host.versionId,
|
|
1522
|
+
viewerKind: viewer.kind,
|
|
1523
|
+
agentDefaultsVersion: SITE_AGENT_DEFAULTS_VERSION,
|
|
1524
|
+
},
|
|
1525
|
+
},
|
|
1526
|
+
...(input.idempotencyKey ? { idempotencyKey: `site-runtime:${requestedSessionId}` } : {}),
|
|
1527
|
+
};
|
|
1528
|
+
const start = () =>
|
|
1529
|
+
send("POST", base, body, dataTools.length > 0 ? SITE_AGENT_DATA_TOOL_PERMISSIONS : []);
|
|
1530
|
+
// Workspace members get no Site budget at all: their conversation is an
|
|
1531
|
+
// ordinary session under the workspace's normal limits, so no session
|
|
1532
|
+
// usage limit is stamped on it, whatever the Site's limits say.
|
|
1533
|
+
const usageLimitMicros =
|
|
1534
|
+
viewer.kind === "member" || repair ? null : reservation.executionLimitMicros;
|
|
1535
|
+
let created: Response;
|
|
1536
|
+
try {
|
|
1537
|
+
// Initial create's limit command is idempotent, not a top-up. A retained
|
|
1538
|
+
// shell must receive its newly funded ceiling before core repairs it.
|
|
1539
|
+
if (repair && viewer.kind !== "member" && reservation.executionLimitMicros !== null) {
|
|
1540
|
+
try {
|
|
1541
|
+
await capSiteConversationUsageLimit(deps, host, requestedSessionId, null);
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
// Dispatch has not begun: this failure is definitive, unlike a
|
|
1544
|
+
// lost response from core's initialization transaction.
|
|
1545
|
+
await finishSiteRuntimeAdmission(deps.db, host.scope, pendingAdmission);
|
|
1546
|
+
pendingAdmission = undefined;
|
|
1547
|
+
const retained = await getOwnedSiteRuntimeConversation(deps.db, host.scope, {
|
|
1548
|
+
sessionId: requestedSessionId,
|
|
1549
|
+
siteId: host.site.id,
|
|
1550
|
+
viewerPrincipal: viewer.principal,
|
|
1551
|
+
});
|
|
1552
|
+
if (retained) await settleSiteRuntimeSpend(deps, host, retained, { release: true });
|
|
1553
|
+
throw error;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
created =
|
|
1557
|
+
usageLimitMicros === null
|
|
1558
|
+
? await start()
|
|
1559
|
+
: await withHostSessionUsageLimit(
|
|
1560
|
+
{ subjectId: `site:${host.site.id}`, limitMicros: usageLimitMicros },
|
|
1561
|
+
start,
|
|
1562
|
+
);
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
await releaseUnstarted(host.scope, requestedSessionId);
|
|
1565
|
+
throw error;
|
|
1566
|
+
}
|
|
1567
|
+
if (created.status !== 202) {
|
|
1568
|
+
await releaseUnstarted(host.scope, requestedSessionId);
|
|
1569
|
+
return siteRuntimeForwardedResponse(created);
|
|
1570
|
+
}
|
|
1571
|
+
const session = (await created.json().catch(() => null)) as { id?: unknown } | null;
|
|
1572
|
+
if (session?.id !== requestedSessionId) {
|
|
1573
|
+
// Never leave a conversation running that its owner record does not track.
|
|
1574
|
+
if (typeof session?.id === "string")
|
|
1575
|
+
await send("POST", `${base}/${session.id}/control`, {
|
|
1576
|
+
action: "cancel",
|
|
1577
|
+
clientEventId: `site-runtime-untracked:${session.id}`,
|
|
1578
|
+
reason: "The conversation could not be tracked",
|
|
1579
|
+
}).then((response) => response.body?.cancel());
|
|
1580
|
+
await releaseUnstarted(host.scope, requestedSessionId);
|
|
1581
|
+
throw sitePublishingError(
|
|
1582
|
+
502,
|
|
1583
|
+
"upstream_unavailable",
|
|
1584
|
+
"site_conversation_failed",
|
|
1585
|
+
"The conversation could not be started",
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
await meterSiteRuntime(deps, host, { type: "conversation", viewer });
|
|
1589
|
+
return context.json(siteSessionProjection(session), 202);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
const sessionId = /^\/sessions\/([^/]+)/u.exec(request.suffix)?.[1] ?? "";
|
|
1593
|
+
const conversation = await getOwnedSiteRuntimeConversation(deps.db, host.scope, {
|
|
1594
|
+
sessionId,
|
|
1595
|
+
siteId: host.site.id,
|
|
1596
|
+
viewerPrincipal: viewer.principal,
|
|
1597
|
+
});
|
|
1598
|
+
if (!conversation) throw notFound();
|
|
1599
|
+
const target = `${base}/${conversation.sessionId}`;
|
|
1600
|
+
// An EventSource reconnect resumes from Last-Event-ID (forwarded by the
|
|
1601
|
+
// gateway); in-process dispatch carries it as the stream's `after` cursor.
|
|
1602
|
+
const streamQuery = () => {
|
|
1603
|
+
const extra: Record<string, string> = {};
|
|
1604
|
+
const lastEventId = request.context.req.header("last-event-id")?.trim();
|
|
1605
|
+
if (!url.searchParams.has("after") && lastEventId && /^\d{1,15}$/u.test(lastEventId))
|
|
1606
|
+
extra.after = lastEventId;
|
|
1607
|
+
return allowedQuery(url, ["after", "transport"], extra);
|
|
1608
|
+
};
|
|
1609
|
+
/**
|
|
1610
|
+
* Event pages and the stream deliver the same Site-safe projection
|
|
1611
|
+
* (site-publishing/session-events.ts): allowlisted event types and
|
|
1612
|
+
* payload fields, with hidden events as placeholders so sequences stay
|
|
1613
|
+
* contiguous. The page is read whole from the API (every type, stored
|
|
1614
|
+
* payloads) because the projection, not the read mode, decides what a
|
|
1615
|
+
* viewer sees, and message text must not be cut to a preview. Page sizes
|
|
1616
|
+
* and the default newest-first order are those of a monitoring read;
|
|
1617
|
+
* `payloadMode=none` drops payloads.
|
|
1618
|
+
*/
|
|
1619
|
+
const payloadNone = url.searchParams.get("payloadMode") === "none";
|
|
1620
|
+
const eventQuery = (includeTypes: string | null) => {
|
|
1621
|
+
const requested = Number(url.searchParams.get("limit") ?? SITE_EVENT_PAGE_DEFAULT);
|
|
1622
|
+
const limit = Number.isFinite(requested)
|
|
1623
|
+
? Math.min(SITE_EVENT_PAGE_MAX, Math.max(1, Math.floor(requested)))
|
|
1624
|
+
: SITE_EVENT_PAGE_DEFAULT;
|
|
1625
|
+
const ordered = ["after", "before", "direction"].some((name) => url.searchParams.has(name));
|
|
1626
|
+
return allowedQuery(url, EVENT_QUERY, {
|
|
1627
|
+
mode: "forensic",
|
|
1628
|
+
payloadMode: payloadNone ? "none" : "full",
|
|
1629
|
+
limit: String(limit),
|
|
1630
|
+
...(ordered ? {} : { direction: "before" }),
|
|
1631
|
+
...(includeTypes ? { includeTypes } : {}),
|
|
1632
|
+
});
|
|
1633
|
+
};
|
|
1634
|
+
/** Requested types a viewer can see; null when none of them is visible. */
|
|
1635
|
+
const visibleIncludeTypes = (): string | null | undefined => {
|
|
1636
|
+
const raw = url.searchParams.get("includeTypes");
|
|
1637
|
+
if (raw === null) return undefined;
|
|
1638
|
+
const visible = raw
|
|
1639
|
+
.split(",")
|
|
1640
|
+
.map((type) => type.trim())
|
|
1641
|
+
.filter((type) => SITE_VISIBLE_SESSION_EVENT_TYPES.has(type));
|
|
1642
|
+
return visible.length > 0 ? [...new Set(visible)].join(",") : null;
|
|
1643
|
+
};
|
|
1644
|
+
// The agent and budget checks every message-bearing operation passes.
|
|
1645
|
+
const admitAgentWork = async (newTurn: boolean) => {
|
|
1646
|
+
requireSiteAgent(deps, host, viewer, limits);
|
|
1647
|
+
if (viewer.kind === "guest" || viewer.kind === "user")
|
|
1648
|
+
await requireSitePlatformOpen(deps, "guest_agent");
|
|
1649
|
+
// Tools and budgets are bound when a conversation starts. One holding a
|
|
1650
|
+
// tool the Site no longer gives this viewer (removed, or the viewer left
|
|
1651
|
+
// the workspace or lost the permission), or counted as a member's while
|
|
1652
|
+
// the viewer no longer is one (or the reverse), takes no more work.
|
|
1653
|
+
const memberPermissions =
|
|
1654
|
+
viewer.kind === "member" && viewer.subjectId
|
|
1655
|
+
? await getSiteWorkspaceMemberPermissions(deps.db, host.scope, viewer.subjectId)
|
|
1656
|
+
: null;
|
|
1657
|
+
const allowedTools = new Set(
|
|
1658
|
+
siteAgentToolRefs(host.publication.agent, viewer, memberPermissions).map((t) => t.id),
|
|
1659
|
+
);
|
|
1660
|
+
// A member's conversation started before members were kept out of the
|
|
1661
|
+
// Site's windows counts in `*` and stays usable; its reservations stay
|
|
1662
|
+
// in the windows they were made in.
|
|
1663
|
+
const legacyMember =
|
|
1664
|
+
viewer.kind === "member" &&
|
|
1665
|
+
conversation.viewerKind === "member" &&
|
|
1666
|
+
conversation.budgetKey === SITE_RUNTIME_SITE_PRINCIPAL;
|
|
1667
|
+
if (
|
|
1668
|
+
(conversation.budgetKey !== siteRuntimeBudgetKey(viewer) && !legacyMember) ||
|
|
1669
|
+
conversation.toolIds.some((tool) => !allowedTools.has(tool))
|
|
1670
|
+
)
|
|
1671
|
+
throw sitePublishingError(
|
|
1672
|
+
409,
|
|
1673
|
+
"conflict",
|
|
1674
|
+
"site_conversation_outdated",
|
|
1675
|
+
"This Site's assistant changed since this conversation started; start a new one",
|
|
1676
|
+
);
|
|
1677
|
+
// People outside the workspace never use a subscription-billed model,
|
|
1678
|
+
// whatever model the conversation holds.
|
|
1679
|
+
if (viewer.kind !== "member") {
|
|
1680
|
+
const session = await getSession(deps.db, host.scope.workspaceId, conversation.sessionId);
|
|
1681
|
+
if (
|
|
1682
|
+
session &&
|
|
1683
|
+
!(await siteCreditModelIds(deps, host.scope, host.site.id)).includes(session.model)
|
|
1684
|
+
)
|
|
1685
|
+
throw siteNotAvailableForViewer(
|
|
1686
|
+
"agent_model",
|
|
1687
|
+
"This Site's assistant cannot use that model for you",
|
|
1688
|
+
{ reason: "subscription_model" },
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
await normalizeSiteAgentSession(
|
|
1692
|
+
deps,
|
|
1693
|
+
host,
|
|
1694
|
+
viewer,
|
|
1695
|
+
conversation.sessionId,
|
|
1696
|
+
conversation.toolIds,
|
|
1697
|
+
);
|
|
1698
|
+
const usage = await settleSiteRuntimeSpend(deps, host, conversation);
|
|
1699
|
+
// Clear the stamped session cap before dropping the durable conversation
|
|
1700
|
+
// marker. Failure refuses admission and preserves the marker for retry.
|
|
1701
|
+
if (viewer.kind === "member" && conversation.limitMicros !== null)
|
|
1702
|
+
await liftSiteConversationUsageLimit(
|
|
1703
|
+
deps,
|
|
1704
|
+
host,
|
|
1705
|
+
conversation.sessionId,
|
|
1706
|
+
conversation.limitMicros,
|
|
1707
|
+
usage,
|
|
1708
|
+
);
|
|
1709
|
+
const admissionId = randomUUID();
|
|
1710
|
+
const admitted = await reserve(() =>
|
|
1711
|
+
reserveSiteRuntimeConversationWork(deps.db, host.scope, {
|
|
1712
|
+
admissionId,
|
|
1713
|
+
sessionId: conversation.sessionId,
|
|
1714
|
+
limits: budgets,
|
|
1715
|
+
newTurn,
|
|
1716
|
+
uncapped: viewer.kind === "member",
|
|
1717
|
+
}),
|
|
1718
|
+
);
|
|
1719
|
+
// Stamp the funded ceiling before work, including atomic top-ups and
|
|
1720
|
+
// reduced caps. Members never carry a Site-imposed usage limit.
|
|
1721
|
+
pendingAdmission = { sessionId: conversation.sessionId, admissionId };
|
|
1722
|
+
try {
|
|
1723
|
+
if (admitted.executionLimitMicros !== null && viewer.kind !== "member")
|
|
1724
|
+
await capSiteConversationUsageLimit(deps, host, conversation.sessionId, usage);
|
|
1725
|
+
} catch (error) {
|
|
1726
|
+
// No dispatch has begun, so failure here is definitive. Reconcile the
|
|
1727
|
+
// queue before refunding: a previous turn may still be executing.
|
|
1728
|
+
await finishSiteRuntimeAdmission(deps.db, host.scope, pendingAdmission);
|
|
1729
|
+
pendingAdmission = undefined;
|
|
1730
|
+
await settleSiteRuntimeSpend(
|
|
1731
|
+
deps,
|
|
1732
|
+
host,
|
|
1733
|
+
{ sessionId: conversation.sessionId, admissions: conversation.admissions + 1 },
|
|
1734
|
+
{ release: true },
|
|
1735
|
+
);
|
|
1736
|
+
throw error;
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
/**
|
|
1740
|
+
* A Site's composer drafts are text only: no workspace resources,
|
|
1741
|
+
* timeline annotations, credentials or connection accounts, and only a
|
|
1742
|
+
* model the Site allows (or the conversation's own).
|
|
1743
|
+
*/
|
|
1744
|
+
const siteDraft = async (
|
|
1745
|
+
draft: Pick<
|
|
1746
|
+
z.infer<typeof SaveComposerDraftRequest>,
|
|
1747
|
+
"text" | "resources" | "annotations" | "model" | "reasoningEffort" | "latencyMode"
|
|
1748
|
+
>,
|
|
1749
|
+
) => {
|
|
1750
|
+
if (draft.text.length > MESSAGE_MAX_CHARS)
|
|
1751
|
+
throw invalid(`A message is at most ${MESSAGE_MAX_CHARS} characters`);
|
|
1752
|
+
if (draft.resources.length > 0 || (draft.annotations?.length ?? 0) > 0)
|
|
1753
|
+
throw invalid("Messages on a published Site are text only");
|
|
1754
|
+
const allowed = new Set((await siteViewerModels(host, viewer, limits)).allowed);
|
|
1755
|
+
// People outside the workspace never use a subscription-billed model.
|
|
1756
|
+
if (viewer.kind !== "member" && isSubscriptionBilledModel([], draft.model))
|
|
1757
|
+
throw siteNotAvailableForViewer(
|
|
1758
|
+
"agent_model",
|
|
1759
|
+
"This Site's assistant cannot use that model for you",
|
|
1760
|
+
{ reason: "subscription_model" },
|
|
1761
|
+
);
|
|
1762
|
+
if (!allowed.has(draft.model)) {
|
|
1763
|
+
const current = await send("GET", target);
|
|
1764
|
+
const session = current.ok
|
|
1765
|
+
? ((await current.json().catch(() => null)) as { model?: unknown } | null)
|
|
1766
|
+
: (await current.body?.cancel(), null);
|
|
1767
|
+
if (session?.model !== draft.model)
|
|
1768
|
+
throw sitePublishingError(
|
|
1769
|
+
400,
|
|
1770
|
+
"validation_failed",
|
|
1771
|
+
"site_model_not_allowed",
|
|
1772
|
+
"This Site's assistant cannot use that model",
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
return {
|
|
1776
|
+
text: draft.text,
|
|
1777
|
+
annotations: [],
|
|
1778
|
+
resources: [],
|
|
1779
|
+
model: draft.model,
|
|
1780
|
+
reasoningEffort: draft.reasoningEffort,
|
|
1781
|
+
latencyMode: draft.latencyMode,
|
|
1782
|
+
};
|
|
1783
|
+
};
|
|
1784
|
+
switch (id) {
|
|
1785
|
+
case "sessions.get":
|
|
1786
|
+
await settleSiteRuntimeSpend(deps, host, conversation);
|
|
1787
|
+
return await projected(await send("GET", target));
|
|
1788
|
+
case "sessions.events": {
|
|
1789
|
+
const payload = payloadNone ? ("none" as const) : ("summary" as const);
|
|
1790
|
+
const includeTypes = visibleIncludeTypes();
|
|
1791
|
+
// Asking only for hidden types yields nothing rather than everything.
|
|
1792
|
+
if (includeTypes === null) return siteSessionEventPage([], { payload });
|
|
1793
|
+
return await siteSessionEventPageResponse(
|
|
1794
|
+
await send("GET", `${target}/events${eventQuery(includeTypes ?? null)}`),
|
|
1795
|
+
{ payload },
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
case "sessions.stream":
|
|
1799
|
+
return siteSessionEventStreamResponse(
|
|
1800
|
+
await send("GET", `${target}/events/stream${streamQuery()}`),
|
|
1801
|
+
);
|
|
1802
|
+
case "sessions.queue":
|
|
1803
|
+
return await forwardView("GET", `${target}/queue`, siteQueueView);
|
|
1804
|
+
case "sessions.humanInput":
|
|
1805
|
+
return await forwardView(
|
|
1806
|
+
"GET",
|
|
1807
|
+
`${target}/human-input-requests${allowedQuery(url, ["status"])}`,
|
|
1808
|
+
siteHumanInputListView,
|
|
1809
|
+
);
|
|
1810
|
+
case "sessions.humanInput.get": {
|
|
1811
|
+
const requestId = /\/human-input-requests\/([^/]+)$/u.exec(request.suffix)?.[1];
|
|
1812
|
+
if (!requestId) throw unsupported();
|
|
1813
|
+
return await forwardView(
|
|
1814
|
+
"GET",
|
|
1815
|
+
`${target}/human-input-requests/${requestId}`,
|
|
1816
|
+
siteHumanInputRequestView,
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
case "sessions.queue.item": {
|
|
1820
|
+
const item = /\/queue\/([^/]+)\/(move|edit|steer|delete)$/u.exec(request.suffix);
|
|
1821
|
+
if (!item) throw unsupported();
|
|
1822
|
+
const [, turnId, action] = item;
|
|
1823
|
+
const path = `${target}/queue/${turnId}/${action}`;
|
|
1824
|
+
switch (action) {
|
|
1825
|
+
case "move": {
|
|
1826
|
+
const move = parseJson(request.body, MoveSessionQueueItemRequest);
|
|
1827
|
+
requireSiteAgent(deps, host, viewer, limits);
|
|
1828
|
+
return await forwardView("POST", path, siteQueueMutationView, move);
|
|
1829
|
+
}
|
|
1830
|
+
case "edit": {
|
|
1831
|
+
const edit = parseJson(request.body, EditSessionQueueItemRequest);
|
|
1832
|
+
requireSiteAgent(deps, host, viewer, limits);
|
|
1833
|
+
return await forwardView("POST", path, siteQueueMutationView, edit);
|
|
1834
|
+
}
|
|
1835
|
+
case "delete": {
|
|
1836
|
+
const remove = parseJson(request.body, DeleteSessionQueueItemRequest);
|
|
1837
|
+
requireSiteAgent(deps, host, viewer, limits);
|
|
1838
|
+
return await forwardView("POST", path, siteQueueMutationView, remove);
|
|
1839
|
+
}
|
|
1840
|
+
default: {
|
|
1841
|
+
// Steering a queued message runs it now: the message was counted
|
|
1842
|
+
// when it was sent, but the agent and spend checks still apply.
|
|
1843
|
+
const steer = parseJson(request.body, SteerSessionQueueItemRequest);
|
|
1844
|
+
await admitAgentWork(false);
|
|
1845
|
+
return await forwardView("POST", path, siteQueueMutationView, steer);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
case "sessions.composerDraft.get":
|
|
1850
|
+
return await forwardView("GET", `${target}/composer-draft`, siteDraftView);
|
|
1851
|
+
case "sessions.composerDraft.put": {
|
|
1852
|
+
const draft = parseJson(request.body, SaveComposerDraftRequest);
|
|
1853
|
+
requireSiteAgent(deps, host, viewer, limits);
|
|
1854
|
+
return await forwardView("PUT", `${target}/composer-draft`, siteDraftView, {
|
|
1855
|
+
...(await siteDraft(draft)),
|
|
1856
|
+
expectedRevision: draft.expectedRevision,
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
case "sessions.composerDraft.submit": {
|
|
1860
|
+
const submit = parseJson(request.body, SubmitComposerDraftRequest);
|
|
1861
|
+
if (
|
|
1862
|
+
submit.modelContext !== undefined ||
|
|
1863
|
+
(submit.mcpCredentialUpdates?.length ?? 0) > 0 ||
|
|
1864
|
+
submit.connectionAccounts.length > 0 ||
|
|
1865
|
+
submit.personalResourceAttachment !== undefined
|
|
1866
|
+
)
|
|
1867
|
+
throw invalid("Messages on a published Site are text only");
|
|
1868
|
+
const draft = await siteDraft(submit);
|
|
1869
|
+
await admitAgentWork(true);
|
|
1870
|
+
return await forwardView(
|
|
1871
|
+
"POST",
|
|
1872
|
+
`${target}/composer-draft/submit`,
|
|
1873
|
+
siteAcceptedMessageView,
|
|
1874
|
+
{
|
|
1875
|
+
...draft,
|
|
1876
|
+
expectedDraftRevision: submit.expectedDraftRevision,
|
|
1877
|
+
clientEventId: submit.clientEventId,
|
|
1878
|
+
delivery: submit.delivery,
|
|
1879
|
+
...(submit.controlEtag ? { controlEtag: submit.controlEtag } : {}),
|
|
1880
|
+
},
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
case "sessions.send": {
|
|
1884
|
+
const message = parseJson(request.body, SendMessageRequest);
|
|
1885
|
+
await admitAgentWork(true);
|
|
1886
|
+
return await forwardView("POST", `${target}/events`, siteAcceptedEventView, {
|
|
1887
|
+
type: "user.message",
|
|
1888
|
+
...(message.clientEventId ? { clientEventId: message.clientEventId } : {}),
|
|
1889
|
+
payload: { text: message.payload.text },
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
case "sessions.steer": {
|
|
1893
|
+
const steer = parseJson(request.body, SteerRequest);
|
|
1894
|
+
await admitAgentWork(false);
|
|
1895
|
+
return await forwardView("POST", `${target}/steer`, siteAcceptedMessageView, {
|
|
1896
|
+
text: steer.text,
|
|
1897
|
+
...(steer.clientEventId ? { clientEventId: steer.clientEventId } : {}),
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
case "sessions.control": {
|
|
1901
|
+
const control = parseJson(request.body, SessionControlRequest);
|
|
1902
|
+
// Pausing or cancelling is always allowed; resuming starts work again.
|
|
1903
|
+
if (control.action === "resume") await admitAgentWork(false);
|
|
1904
|
+
const controlled = await send("POST", `${target}/control`, {
|
|
1905
|
+
action: control.action,
|
|
1906
|
+
clientEventId: control.clientEventId,
|
|
1907
|
+
...(control.reason ? { reason: control.reason } : {}),
|
|
1908
|
+
});
|
|
1909
|
+
// Settled after the control applied: a cancelled conversation releases
|
|
1910
|
+
// its reservation at once.
|
|
1911
|
+
if (control.action !== "resume" && controlled.ok)
|
|
1912
|
+
await settleSiteRuntimeSpend(deps, host, conversation);
|
|
1913
|
+
if (!controlled.ok) return siteRuntimeForwardedResponse(controlled);
|
|
1914
|
+
return context.json(
|
|
1915
|
+
siteControlResultView(await controlled.json()) as object,
|
|
1916
|
+
controlled.status as 200,
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
default:
|
|
1920
|
+
throw unsupported();
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
|
|
1924
|
+
/**
|
|
1925
|
+
* Undo a conversation start whose session does not exist (the create was
|
|
1926
|
+
* refused or failed before commit). A session that does exist stays
|
|
1927
|
+
* tracked: its usage limit caps it and settling releases what it does not use.
|
|
1928
|
+
*/
|
|
1929
|
+
const releaseUnstarted = async (
|
|
1930
|
+
scope: { accountId: string; workspaceId: string },
|
|
1931
|
+
sessionId: string,
|
|
1932
|
+
) => {
|
|
1933
|
+
// A failed read keeps the reservation: settling releases it once the
|
|
1934
|
+
// session is known to be missing or idle.
|
|
1935
|
+
const exists = await getSession(deps.db, scope.workspaceId, sessionId).then(
|
|
1936
|
+
(session) => session !== null,
|
|
1937
|
+
() => true,
|
|
1938
|
+
);
|
|
1939
|
+
if (!exists) await releaseSiteRuntimeConversationStart(deps.db, scope, { sessionId });
|
|
1940
|
+
};
|
|
1941
|
+
|
|
1942
|
+
app.all(SITE_RUNTIME_API_PREFIX, handler);
|
|
1943
|
+
app.all(`${SITE_RUNTIME_API_PREFIX}/*`, handler);
|
|
1944
|
+
}
|