@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,1282 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
normalizeSitesDomain,
|
|
4
|
+
OPENGENI_API_CONTRACT_HEADER,
|
|
5
|
+
OPENGENI_API_CONTRACT_REVISION,
|
|
6
|
+
signDelegatedAccessToken,
|
|
7
|
+
siteHostname,
|
|
8
|
+
siteHostnameLabelProblem,
|
|
9
|
+
SITE_HOSTNAME_PROBLEM_MESSAGES,
|
|
10
|
+
suggestSiteHostnameLabel,
|
|
11
|
+
type ErrorCode,
|
|
12
|
+
type Permission,
|
|
13
|
+
type SiteHostMapRecord,
|
|
14
|
+
type SiteHostnameAvailability,
|
|
15
|
+
type SitePublication,
|
|
16
|
+
type UpsertSitePublicationRequest,
|
|
17
|
+
} from "@opengeni/contracts";
|
|
18
|
+
import type { AccessGrant } from "@opengeni/contracts";
|
|
19
|
+
import {
|
|
20
|
+
assertNotSiteClampedAgent,
|
|
21
|
+
currentSiteSessionOrigin,
|
|
22
|
+
hasLiteralPermission,
|
|
23
|
+
siteAgentClamp,
|
|
24
|
+
siteViewerForGrant,
|
|
25
|
+
type ApiRouteDeps,
|
|
26
|
+
} from "@opengeni/core";
|
|
27
|
+
import {
|
|
28
|
+
bumpSitePublicationGeneration,
|
|
29
|
+
checkSiteHostnameClaim,
|
|
30
|
+
countOpenSiteAbuseReports,
|
|
31
|
+
getPublishedSiteVersion,
|
|
32
|
+
getSiteDataSite,
|
|
33
|
+
artifactCreatedInSessionTree,
|
|
34
|
+
getSitePublication,
|
|
35
|
+
isSitePublishingUuid,
|
|
36
|
+
getWorkspaceSiteLimitProfile,
|
|
37
|
+
markSitePublicationEdgeSynced,
|
|
38
|
+
readSiteRuntimeUsage,
|
|
39
|
+
recordSitePublicationScan,
|
|
40
|
+
getPublishedSite,
|
|
41
|
+
listActiveSiteRuntimeConversations,
|
|
42
|
+
revokeSiteViewerSessions,
|
|
43
|
+
SitePublishingError,
|
|
44
|
+
SITE_RUNTIME_SITE_PRINCIPAL,
|
|
45
|
+
upsertSitePublication,
|
|
46
|
+
readSiteHostnameEdgeState,
|
|
47
|
+
readSiteHostnameLiveGeneration,
|
|
48
|
+
type PublishedSiteRecord,
|
|
49
|
+
type SiteDataSite,
|
|
50
|
+
type SiteHostnameEdgeState,
|
|
51
|
+
type SitePublicationRecord,
|
|
52
|
+
type SitePublishingScope,
|
|
53
|
+
type StoredSiteLimitProfile,
|
|
54
|
+
} from "@opengeni/db";
|
|
55
|
+
import { resolveFirstPartyDelegationSecret } from "@opengeni/config";
|
|
56
|
+
import { HTTPException } from "hono/http-exception";
|
|
57
|
+
import { ApiHttpError } from "../http/api-error";
|
|
58
|
+
import { siteDataRole } from "../site-data/service";
|
|
59
|
+
import { createSiteContentScanner, type SiteContentScanner } from "./content-scanner";
|
|
60
|
+
import { createSiteEdgeSync, type SiteEdgeSync } from "./edge-sync";
|
|
61
|
+
import { effectiveSiteLimits, sourceSiteLimitProfile, storedSiteLimitProfileKind } from "./limits";
|
|
62
|
+
import { requireSitePlatformOpen, requireSitePlatformQuota } from "./platform-usage";
|
|
63
|
+
import { readSitePublicationReadiness } from "./readiness";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Published Sites: publication management, the host map, and the work that
|
|
67
|
+
* follows a change (edge sync, content scan). Postgres is the source of truth;
|
|
68
|
+
* the edge copies are rebuildable and written after commit, never blocking it.
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
export type SitePublishingDeps = Pick<ApiRouteDeps, "db" | "settings" | "objectStorage"> &
|
|
72
|
+
Partial<Pick<ApiRouteDeps, "managedEmailTransport">>;
|
|
73
|
+
|
|
74
|
+
// --- configuration -------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/** The configured Sites domain, or null when publishing is off (unset or invalid). */
|
|
77
|
+
export function configuredSitesDomain(settings: ApiRouteDeps["settings"]): string | null {
|
|
78
|
+
if (!settings.sitesDomain) return null;
|
|
79
|
+
try {
|
|
80
|
+
return normalizeSitesDomain(settings.sitesDomain);
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function sitePublishingError(
|
|
87
|
+
status: number,
|
|
88
|
+
code: ErrorCode,
|
|
89
|
+
detail: string,
|
|
90
|
+
message: string,
|
|
91
|
+
extra: Record<string, unknown> = {},
|
|
92
|
+
): ApiHttpError {
|
|
93
|
+
return new ApiHttpError(status, {
|
|
94
|
+
code,
|
|
95
|
+
message,
|
|
96
|
+
retryable: false,
|
|
97
|
+
outcomeUnknown: false,
|
|
98
|
+
details: { ...extra, code: detail },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The Sites domain, or 404 when this deployment does not publish Sites. */
|
|
103
|
+
export function requireSitesDomain(settings: ApiRouteDeps["settings"]): string {
|
|
104
|
+
const domain = configuredSitesDomain(settings);
|
|
105
|
+
if (!domain)
|
|
106
|
+
throw sitePublishingError(
|
|
107
|
+
404,
|
|
108
|
+
"not_found",
|
|
109
|
+
"sites_publishing_disabled",
|
|
110
|
+
"Publishing Sites is not enabled on this deployment",
|
|
111
|
+
);
|
|
112
|
+
return domain;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const PUBLIC_CODE: Record<SitePublishingError["code"], ErrorCode> = {
|
|
116
|
+
not_found: "not_found",
|
|
117
|
+
taken: "conflict",
|
|
118
|
+
held: "conflict",
|
|
119
|
+
reserved: "conflict",
|
|
120
|
+
conflict: "conflict",
|
|
121
|
+
forbidden: "forbidden",
|
|
122
|
+
validation: "validation_failed",
|
|
123
|
+
};
|
|
124
|
+
const STATUS: Record<SitePublishingError["code"], number> = {
|
|
125
|
+
not_found: 404,
|
|
126
|
+
taken: 409,
|
|
127
|
+
held: 409,
|
|
128
|
+
reserved: 409,
|
|
129
|
+
conflict: 409,
|
|
130
|
+
forbidden: 403,
|
|
131
|
+
validation: 400,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export function sitePublishingHttpError(error: SitePublishingError): ApiHttpError {
|
|
135
|
+
return sitePublishingError(
|
|
136
|
+
STATUS[error.code],
|
|
137
|
+
PUBLIC_CODE[error.code],
|
|
138
|
+
`site_${error.code}`,
|
|
139
|
+
error.message,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function runSitePublishing<T>(fn: () => Promise<T>): Promise<T> {
|
|
144
|
+
try {
|
|
145
|
+
return await fn();
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof SitePublishingError) throw sitePublishingHttpError(error);
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --- after-commit effects --------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
export type SitePublishingEffects = {
|
|
155
|
+
edgeSync: SiteEdgeSync;
|
|
156
|
+
scanner: SiteContentScanner;
|
|
157
|
+
/** Wait for every scheduled effect (tests and graceful shutdown). */
|
|
158
|
+
flush(): Promise<void>;
|
|
159
|
+
/**
|
|
160
|
+
* Run `run` in the background, at most `SITE_EFFECT_CONCURRENCY` at a time;
|
|
161
|
+
* the rest wait in a bounded queue (beyond it a task is dropped and logged:
|
|
162
|
+
* edge copies self-heal and expire, scans and notices are best effort).
|
|
163
|
+
* Returns false when the task was dropped, so callers can release what
|
|
164
|
+
* they reserved for it.
|
|
165
|
+
*/
|
|
166
|
+
schedule(name: string, run: () => Promise<void>): boolean;
|
|
167
|
+
/** Hostnames with an edge sync running or queued, and what it still owes. */
|
|
168
|
+
edgeLabels: Map<string, { again: boolean; purge: boolean; siteIdHint: string | null }>;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/** Background effects running at once per process. */
|
|
172
|
+
export const SITE_EFFECT_CONCURRENCY = 4;
|
|
173
|
+
/** Background effects waiting per process before new ones are dropped. */
|
|
174
|
+
const SITE_EFFECT_QUEUE_LIMIT = 2_000;
|
|
175
|
+
|
|
176
|
+
const effectsBySettings = new WeakMap<object, SitePublishingEffects>();
|
|
177
|
+
const dispatchBySettings = new WeakMap<object, (request: Request) => Promise<Response>>();
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The app's in-process dispatch, registered with the site-runtime routes, so
|
|
181
|
+
* publication changes can cancel a Site's conversations through the ordinary
|
|
182
|
+
* session routes. Without it (the operator script) cancellation is skipped.
|
|
183
|
+
*/
|
|
184
|
+
export function setSitePublishingDispatch(
|
|
185
|
+
settings: ApiRouteDeps["settings"],
|
|
186
|
+
dispatch: (request: Request) => Promise<Response>,
|
|
187
|
+
): void {
|
|
188
|
+
dispatchBySettings.set(settings, dispatch);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function sitePublishingDispatch(
|
|
192
|
+
settings: ApiRouteDeps["settings"],
|
|
193
|
+
): ((request: Request) => Promise<Response>) | null {
|
|
194
|
+
return dispatchBySettings.get(settings) ?? null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
type SitePublishingEffectsOptions = {
|
|
198
|
+
fetch?: typeof fetch;
|
|
199
|
+
backoffMs?: number;
|
|
200
|
+
scanner?: SiteContentScanner;
|
|
201
|
+
/** Waiting tasks before new ones are dropped (tests fill a small queue). */
|
|
202
|
+
queueLimit?: number;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
function createEffects(
|
|
206
|
+
settings: ApiRouteDeps["settings"],
|
|
207
|
+
options: SitePublishingEffectsOptions = {},
|
|
208
|
+
): SitePublishingEffects {
|
|
209
|
+
const queueLimit = options.queueLimit ?? SITE_EFFECT_QUEUE_LIMIT;
|
|
210
|
+
const pending = new Set<Promise<void>>();
|
|
211
|
+
const waiting: Array<() => void> = [];
|
|
212
|
+
let running = 0;
|
|
213
|
+
return {
|
|
214
|
+
edgeLabels: new Map(),
|
|
215
|
+
edgeSync: createSiteEdgeSync(settings, {
|
|
216
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
217
|
+
...(options.backoffMs !== undefined ? { backoffMs: options.backoffMs } : {}),
|
|
218
|
+
}),
|
|
219
|
+
scanner:
|
|
220
|
+
options.scanner ??
|
|
221
|
+
createSiteContentScanner(settings, options.fetch ? { fetch: options.fetch } : {}),
|
|
222
|
+
async flush() {
|
|
223
|
+
while (pending.size > 0) await Promise.allSettled([...pending]);
|
|
224
|
+
},
|
|
225
|
+
schedule(name, run) {
|
|
226
|
+
if (running >= SITE_EFFECT_CONCURRENCY && waiting.length >= queueLimit) {
|
|
227
|
+
console.warn(`[site-publishing] ${name} dropped: too many pending effects`);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
const task = new Promise<void>((resolve) => {
|
|
231
|
+
const start = async () => {
|
|
232
|
+
running += 1;
|
|
233
|
+
try {
|
|
234
|
+
await run();
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.warn(`[site-publishing] ${name} failed`, {
|
|
237
|
+
error: error instanceof Error ? error.message : String(error),
|
|
238
|
+
});
|
|
239
|
+
} finally {
|
|
240
|
+
running -= 1;
|
|
241
|
+
resolve();
|
|
242
|
+
waiting.shift()?.();
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
if (running < SITE_EFFECT_CONCURRENCY) void start();
|
|
246
|
+
else waiting.push(() => void start());
|
|
247
|
+
});
|
|
248
|
+
pending.add(task);
|
|
249
|
+
void task.finally(() => pending.delete(task));
|
|
250
|
+
return true;
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The effects for this deployment's settings (one set per settings object). */
|
|
256
|
+
export function sitePublishingEffects(settings: ApiRouteDeps["settings"]): SitePublishingEffects {
|
|
257
|
+
let effects = effectsBySettings.get(settings);
|
|
258
|
+
if (!effects) {
|
|
259
|
+
effects = createEffects(settings);
|
|
260
|
+
effectsBySettings.set(settings, effects);
|
|
261
|
+
}
|
|
262
|
+
return effects;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Replace the effects for a settings object (tests: a fake fetch or scanner). */
|
|
266
|
+
export function configureSitePublishingEffects(
|
|
267
|
+
settings: ApiRouteDeps["settings"],
|
|
268
|
+
options: SitePublishingEffectsOptions,
|
|
269
|
+
): SitePublishingEffects {
|
|
270
|
+
const effects = createEffects(settings, options);
|
|
271
|
+
effectsBySettings.set(settings, effects);
|
|
272
|
+
return effects;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Whether a Site may be served at all: active and visible to its workspace. */
|
|
276
|
+
export function siteServable(site: Pick<PublishedSiteRecord, "status" | "visibility">): boolean {
|
|
277
|
+
return site.status === "active" && site.visibility === "workspace_shared";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function siteHostMapRecord(
|
|
281
|
+
settings: ApiRouteDeps["settings"],
|
|
282
|
+
domain: string,
|
|
283
|
+
publication: SitePublicationRecord,
|
|
284
|
+
site: PublishedSiteRecord,
|
|
285
|
+
): SiteHostMapRecord {
|
|
286
|
+
const status =
|
|
287
|
+
publication.status === "active" && !siteServable(site) ? "unpublished" : publication.status;
|
|
288
|
+
return {
|
|
289
|
+
v: 1,
|
|
290
|
+
hostname: siteHostname(publication.label, domain),
|
|
291
|
+
siteId: publication.siteId,
|
|
292
|
+
status,
|
|
293
|
+
access: publication.access,
|
|
294
|
+
versionId: site.currentVersionId,
|
|
295
|
+
title: site.title,
|
|
296
|
+
noindex: publication.noindex || settings.sitesForceNoindex,
|
|
297
|
+
generation: publication.generation,
|
|
298
|
+
updatedAt: publication.updatedAt.toISOString(),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The host-map record a hostname should have now: the live Site's, else a
|
|
304
|
+
* tombstone (unpublished, no version) with a fresh generation, so it replaces
|
|
305
|
+
* whatever the edge holds for that hostname. Null when no Site is known.
|
|
306
|
+
*/
|
|
307
|
+
export function siteHostnameEdgeRecord(
|
|
308
|
+
settings: ApiRouteDeps["settings"],
|
|
309
|
+
domain: string,
|
|
310
|
+
state: SiteHostnameEdgeState,
|
|
311
|
+
siteIdHint: string | null,
|
|
312
|
+
): SiteHostMapRecord | null {
|
|
313
|
+
if (state.live)
|
|
314
|
+
return siteHostMapRecord(settings, domain, state.live.publication, state.live.site);
|
|
315
|
+
const siteId = state.hostname?.siteId ?? state.hold?.siteId ?? siteIdHint;
|
|
316
|
+
if (!siteId || state.tombstoneGeneration === null) return null;
|
|
317
|
+
return {
|
|
318
|
+
v: 1,
|
|
319
|
+
hostname: siteHostname(state.label, domain),
|
|
320
|
+
siteId,
|
|
321
|
+
status: state.hold?.kind === "platform" ? "suspended" : "unpublished",
|
|
322
|
+
access: "members",
|
|
323
|
+
versionId: null,
|
|
324
|
+
title: "",
|
|
325
|
+
noindex: true,
|
|
326
|
+
generation: state.tombstoneGeneration,
|
|
327
|
+
updatedAt: new Date().toISOString(),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** When each label last asked for a self-heal sync (per process). */
|
|
332
|
+
const recentHeals = new Map<string, number>();
|
|
333
|
+
/** Self-heal syncs started in the current minute (per process). */
|
|
334
|
+
const healWindow = { startedAt: 0, count: 0 };
|
|
335
|
+
/** At most this many self-heal syncs per minute per process. */
|
|
336
|
+
const SITE_EDGE_HEALS_PER_MINUTE = 60;
|
|
337
|
+
/** Re-reads after an edge write before leaving the rest to the next sync. */
|
|
338
|
+
const SITE_EDGE_SYNC_ROUNDS = 3;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Sync one hostname to the edge: read what it should hold (under the
|
|
342
|
+
* hostname's lock, in a short transaction that commits before any HTTP),
|
|
343
|
+
* write the live Site's record or a tombstone, purge the edge cache when
|
|
344
|
+
* asked, and record the generation the edge holds. Then re-read the label's
|
|
345
|
+
* live generation; if it moved while the HTTP calls ran, write again (up to
|
|
346
|
+
* `SITE_EDGE_SYNC_ROUNDS` times). No database transaction or lock is held
|
|
347
|
+
* during the Cloudflare calls. A no-op without edge settings.
|
|
348
|
+
*/
|
|
349
|
+
export async function syncSiteHostnameEdge(
|
|
350
|
+
deps: SitePublishingDeps,
|
|
351
|
+
label: string,
|
|
352
|
+
options: { purge?: boolean; siteIdHint?: string | null } = {},
|
|
353
|
+
): Promise<"synced" | "skipped" | "disabled"> {
|
|
354
|
+
const domain = configuredSitesDomain(deps.settings);
|
|
355
|
+
const effects = sitePublishingEffects(deps.settings);
|
|
356
|
+
if (!domain || (!effects.edgeSync.kvEnabled && !effects.edgeSync.purgeEnabled)) return "disabled";
|
|
357
|
+
let outcome: "synced" | "skipped" = "skipped";
|
|
358
|
+
let purge = options.purge === true;
|
|
359
|
+
for (let round = 0; round < SITE_EDGE_SYNC_ROUNDS; round += 1) {
|
|
360
|
+
const state = await readSiteHostnameEdgeState(deps.db, label);
|
|
361
|
+
const record = siteHostnameEdgeRecord(deps.settings, domain, state, options.siteIdHint ?? null);
|
|
362
|
+
if (!record) return outcome;
|
|
363
|
+
const put = await effects.edgeSync.putHostRecord(record);
|
|
364
|
+
if (purge) {
|
|
365
|
+
await effects.edgeSync.purgeHost(record.hostname);
|
|
366
|
+
purge = false;
|
|
367
|
+
}
|
|
368
|
+
if (state.live && put !== "disabled") {
|
|
369
|
+
const { publication } = state.live;
|
|
370
|
+
await markSitePublicationEdgeSynced(
|
|
371
|
+
deps.db,
|
|
372
|
+
{ accountId: publication.accountId, workspaceId: publication.workspaceId },
|
|
373
|
+
publication.siteId,
|
|
374
|
+
publication.generation,
|
|
375
|
+
);
|
|
376
|
+
outcome = "synced";
|
|
377
|
+
}
|
|
378
|
+
const now = await readSiteHostnameLiveGeneration(deps.db, label);
|
|
379
|
+
const unchanged = state.live
|
|
380
|
+
? now?.siteId === state.live.publication.siteId &&
|
|
381
|
+
now.generation === state.live.publication.generation
|
|
382
|
+
: now === null;
|
|
383
|
+
if (unchanged) return outcome;
|
|
384
|
+
}
|
|
385
|
+
return outcome;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* After commit: rewrite the edge copy of each hostname (the Site's label and
|
|
390
|
+
* a released previous one), and purge their edge cache when asked
|
|
391
|
+
* (takedown, unpublish). A no-op without edge settings; failures are retried
|
|
392
|
+
* and logged, and a missed sync heals on the next host lookup. Lost if the
|
|
393
|
+
* process stops first: the KV TTL then bounds how long the edge stays stale.
|
|
394
|
+
* One sync per hostname at a time; background effects run with bounded
|
|
395
|
+
* concurrency (`SITE_EFFECT_CONCURRENCY`).
|
|
396
|
+
*/
|
|
397
|
+
export function scheduleSiteEdgeSync(
|
|
398
|
+
deps: SitePublishingDeps,
|
|
399
|
+
labels: ReadonlyArray<string | null | undefined>,
|
|
400
|
+
options: { purge?: boolean; siteIdHint?: string | null } = {},
|
|
401
|
+
): void {
|
|
402
|
+
const domain = configuredSitesDomain(deps.settings);
|
|
403
|
+
const effects = sitePublishingEffects(deps.settings);
|
|
404
|
+
if (!domain || (!effects.edgeSync.kvEnabled && !effects.edgeSync.purgeEnabled)) return;
|
|
405
|
+
const unique = [...new Set(labels.filter((label): label is string => Boolean(label)))];
|
|
406
|
+
for (const label of unique) {
|
|
407
|
+
// At most one sync in flight per hostname: a change while one runs makes
|
|
408
|
+
// it run again (with the purge it now owes) instead of starting another.
|
|
409
|
+
const owed = effects.edgeLabels.get(label);
|
|
410
|
+
if (owed) {
|
|
411
|
+
owed.again = true;
|
|
412
|
+
owed.purge ||= options.purge === true;
|
|
413
|
+
owed.siteIdHint ??= options.siteIdHint ?? null;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const entry = {
|
|
417
|
+
again: false,
|
|
418
|
+
purge: options.purge === true,
|
|
419
|
+
siteIdHint: options.siteIdHint ?? null,
|
|
420
|
+
};
|
|
421
|
+
effects.edgeLabels.set(label, entry);
|
|
422
|
+
const queued = effects.schedule("edge sync", async () => {
|
|
423
|
+
try {
|
|
424
|
+
for (let pass = 0; pass < SITE_EDGE_SYNC_ROUNDS; pass += 1) {
|
|
425
|
+
const purge = entry.purge;
|
|
426
|
+
entry.again = false;
|
|
427
|
+
entry.purge = false;
|
|
428
|
+
await syncSiteHostnameEdge(deps, label, { purge, siteIdHint: entry.siteIdHint });
|
|
429
|
+
if (!entry.again) return;
|
|
430
|
+
}
|
|
431
|
+
} finally {
|
|
432
|
+
effects.edgeLabels.delete(label);
|
|
433
|
+
// Still owed after the last pass: queue one more sync for it.
|
|
434
|
+
if (entry.again)
|
|
435
|
+
scheduleSiteEdgeSync(deps, [label], {
|
|
436
|
+
purge: entry.purge,
|
|
437
|
+
siteIdHint: entry.siteIdHint,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
// Dropped (the queue is full): release the hostname, so a later change
|
|
442
|
+
// or the self-heal can sync it again.
|
|
443
|
+
if (!queued) effects.edgeLabels.delete(label);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Self-heal: the edge copy of a live hostname is behind its publication
|
|
449
|
+
* (a sync was lost or failed). Schedules one sync per hostname at most every
|
|
450
|
+
* 30 seconds, and at most `SITE_EDGE_HEALS_PER_MINUTE` in all, per process;
|
|
451
|
+
* none while a sync for that hostname is already running.
|
|
452
|
+
*/
|
|
453
|
+
export function healSiteEdgeIfStale(
|
|
454
|
+
deps: SitePublishingDeps,
|
|
455
|
+
publication: Pick<
|
|
456
|
+
SitePublicationRecord,
|
|
457
|
+
"label" | "generation" | "edgeSyncedGeneration" | "siteId"
|
|
458
|
+
>,
|
|
459
|
+
): void {
|
|
460
|
+
if (publication.edgeSyncedGeneration >= publication.generation) return;
|
|
461
|
+
const effects = sitePublishingEffects(deps.settings);
|
|
462
|
+
if (!effects.edgeSync.kvEnabled || effects.edgeLabels.has(publication.label)) return;
|
|
463
|
+
const now = Date.now();
|
|
464
|
+
if ((recentHeals.get(publication.label) ?? 0) > now - 30_000) return;
|
|
465
|
+
if (healWindow.startedAt <= now - 60_000) {
|
|
466
|
+
healWindow.startedAt = now;
|
|
467
|
+
healWindow.count = 0;
|
|
468
|
+
}
|
|
469
|
+
if (healWindow.count >= SITE_EDGE_HEALS_PER_MINUTE) return;
|
|
470
|
+
healWindow.count += 1;
|
|
471
|
+
if (recentHeals.size > 10_000) recentHeals.clear();
|
|
472
|
+
recentHeals.set(publication.label, now);
|
|
473
|
+
scheduleSiteEdgeSync(deps, [publication.label], { siteIdHint: publication.siteId });
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* After commit: scan the URLs in the published version's HTML with the
|
|
478
|
+
* configured scanner and record the findings. Never blocks publishing; the
|
|
479
|
+
* `none` scanner records nothing.
|
|
480
|
+
*/
|
|
481
|
+
export function scheduleSiteContentScan(
|
|
482
|
+
deps: SitePublishingDeps,
|
|
483
|
+
scope: SitePublishingScope,
|
|
484
|
+
siteId: string,
|
|
485
|
+
versionId: string | null,
|
|
486
|
+
): void {
|
|
487
|
+
const effects = sitePublishingEffects(deps.settings);
|
|
488
|
+
if (effects.scanner.name === "none" || !versionId || !deps.objectStorage) return;
|
|
489
|
+
const storage = deps.objectStorage;
|
|
490
|
+
effects.schedule("content scan", async () => {
|
|
491
|
+
const version = await getPublishedSiteVersion(deps.db, scope, siteId, versionId);
|
|
492
|
+
if (!version) return;
|
|
493
|
+
const object = await storage.getObjectBytes(version.contentKey);
|
|
494
|
+
if (!object) return;
|
|
495
|
+
const html = new TextDecoder().decode(object.bytes.slice(0, 2 * 1024 * 1024));
|
|
496
|
+
const result = await effects.scanner.scanHtml(html);
|
|
497
|
+
await recordSitePublicationScan(deps.db, scope, siteId, {
|
|
498
|
+
versionId,
|
|
499
|
+
status: result.status,
|
|
500
|
+
findings: result.findings,
|
|
501
|
+
});
|
|
502
|
+
const domain = configuredSitesDomain(deps.settings);
|
|
503
|
+
const publication =
|
|
504
|
+
result.status === "flagged" && domain
|
|
505
|
+
? await getSitePublication(deps.db, scope, siteId)
|
|
506
|
+
: null;
|
|
507
|
+
if (publication && domain)
|
|
508
|
+
notifySiteAbuse(deps, {
|
|
509
|
+
scope,
|
|
510
|
+
siteId,
|
|
511
|
+
hostname: siteHostname(publication.label, domain),
|
|
512
|
+
kind: "flagged_scan",
|
|
513
|
+
detail: `${result.findings.length} finding(s) in version ${versionId}`,
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* A Site changed outside publishing (new version, rollback, archive, audience):
|
|
520
|
+
* advance its publication generation and resync the edge, and scan a newly
|
|
521
|
+
* current version. A no-op when the Site is not published. Never throws.
|
|
522
|
+
*/
|
|
523
|
+
export async function notifySitePublicationChanged(
|
|
524
|
+
deps: SitePublishingDeps,
|
|
525
|
+
scope: SitePublishingScope,
|
|
526
|
+
siteId: string,
|
|
527
|
+
options: { scanVersionId?: string | null } = {},
|
|
528
|
+
): Promise<void> {
|
|
529
|
+
if (!configuredSitesDomain(deps.settings)) return;
|
|
530
|
+
try {
|
|
531
|
+
const publication = await bumpSitePublicationGeneration(deps.db, scope, siteId);
|
|
532
|
+
if (!publication) return;
|
|
533
|
+
// Archived or made private: the Site is no longer served, so its viewers stop too.
|
|
534
|
+
if (publication.status === "active") {
|
|
535
|
+
const site = await getPublishedSite(deps.db, scope, siteId);
|
|
536
|
+
if (!site || !siteServable(site))
|
|
537
|
+
await stopSiteViewers(deps, scope, siteId, { reason: "unpublished" });
|
|
538
|
+
}
|
|
539
|
+
scheduleSiteEdgeSync(deps, [publication.label], { siteIdHint: siteId });
|
|
540
|
+
if (options.scanVersionId && publication.status === "active")
|
|
541
|
+
scheduleSiteContentScan(deps, scope, siteId, options.scanVersionId);
|
|
542
|
+
} catch (error) {
|
|
543
|
+
console.warn("[site-publishing] could not record a Site change", {
|
|
544
|
+
siteId,
|
|
545
|
+
error: error instanceof Error ? error.message : String(error),
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* The one hook every Site artifact mutation runs, from the API routes and
|
|
552
|
+
* the agent tools alike (publish, rollback, archive, restore): a published
|
|
553
|
+
* Site follows its Site. `scan` scans the version that became current
|
|
554
|
+
* (publish and rollback). Replays changed nothing and do nothing.
|
|
555
|
+
*/
|
|
556
|
+
export async function afterSiteArtifactMutation<
|
|
557
|
+
T extends {
|
|
558
|
+
replayed?: boolean;
|
|
559
|
+
artifact: { id: string; accountId: string; workspaceId: string };
|
|
560
|
+
version?: { id: string } | null;
|
|
561
|
+
},
|
|
562
|
+
>(deps: SitePublishingDeps, result: T, options: { scan?: boolean } = {}): Promise<T> {
|
|
563
|
+
if (!result.replayed)
|
|
564
|
+
await notifySitePublicationChanged(
|
|
565
|
+
deps,
|
|
566
|
+
{ accountId: result.artifact.accountId, workspaceId: result.artifact.workspaceId },
|
|
567
|
+
result.artifact.id,
|
|
568
|
+
options.scan && result.version ? { scanVersionId: result.version.id } : {},
|
|
569
|
+
);
|
|
570
|
+
return result;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Refuse a new version of a published (active) Site while the Opengeni-wide
|
|
575
|
+
* platform brake is engaged. Sites that are not published, rollbacks and
|
|
576
|
+
* unpublishing are never braked.
|
|
577
|
+
*/
|
|
578
|
+
export async function requireSiteVersionPublishOpen(
|
|
579
|
+
deps: SitePublishingDeps,
|
|
580
|
+
scope: SitePublishingScope,
|
|
581
|
+
siteId: string,
|
|
582
|
+
): Promise<void> {
|
|
583
|
+
if (!isSitePublishingUuid(siteId)) return;
|
|
584
|
+
const publication = await getSitePublication(deps.db, scope, siteId);
|
|
585
|
+
if (publication?.status === "active") await requireSitePlatformOpen(deps, "publish");
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// --- the Site's own principal ---------------------------------------------------------
|
|
589
|
+
|
|
590
|
+
/** The authority Site agent sessions run with: sessions only, nothing else. */
|
|
591
|
+
export const SITE_RUNTIME_AGENT_PERMISSIONS: readonly Permission[] = Object.freeze([
|
|
592
|
+
"workspace:read",
|
|
593
|
+
"sessions:read",
|
|
594
|
+
"sessions:create",
|
|
595
|
+
"sessions:control",
|
|
596
|
+
]);
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* A request to an ordinary session route as the Site's service principal
|
|
600
|
+
* (`site:<siteId>`), with a 60-second delegated token carrying only session
|
|
601
|
+
* permissions, for in-process dispatch. The Site headers ride along so the
|
|
602
|
+
* API's Site frame guard and provenance see which Site acts.
|
|
603
|
+
* `extraPermissions` adds what one request must delegate onward (a
|
|
604
|
+
* conversation start delegates its Site data tools' permissions).
|
|
605
|
+
*/
|
|
606
|
+
export async function siteServiceSessionRequest(
|
|
607
|
+
deps: Pick<SitePublishingDeps, "settings">,
|
|
608
|
+
scope: SitePublishingScope,
|
|
609
|
+
site: { id: string; versionId: string | null },
|
|
610
|
+
input: {
|
|
611
|
+
origin: string;
|
|
612
|
+
method: string;
|
|
613
|
+
path: string;
|
|
614
|
+
body?: unknown;
|
|
615
|
+
label: string;
|
|
616
|
+
extraPermissions?: readonly Permission[];
|
|
617
|
+
},
|
|
618
|
+
): Promise<Request> {
|
|
619
|
+
const secret = resolveFirstPartyDelegationSecret(deps.settings);
|
|
620
|
+
if (!secret)
|
|
621
|
+
throw sitePublishingError(
|
|
622
|
+
503,
|
|
623
|
+
"upstream_unavailable",
|
|
624
|
+
"site_agents_unavailable",
|
|
625
|
+
"Site assistants are not configured on this deployment",
|
|
626
|
+
{ reason: "delegation_unconfigured" },
|
|
627
|
+
);
|
|
628
|
+
const token = await signDelegatedAccessToken(secret, {
|
|
629
|
+
accountId: scope.accountId,
|
|
630
|
+
workspaceId: scope.workspaceId,
|
|
631
|
+
subjectId: `site:${site.id}`,
|
|
632
|
+
subjectLabel: input.label.slice(0, 250),
|
|
633
|
+
permissions: [
|
|
634
|
+
...new Set([...SITE_RUNTIME_AGENT_PERMISSIONS, ...(input.extraPermissions ?? [])]),
|
|
635
|
+
],
|
|
636
|
+
principalKind: "service",
|
|
637
|
+
exp: Math.floor(Date.now() / 1000) + 60,
|
|
638
|
+
});
|
|
639
|
+
const headers = new Headers({
|
|
640
|
+
authorization: `Bearer ${token}`,
|
|
641
|
+
accept: "application/json",
|
|
642
|
+
[OPENGENI_API_CONTRACT_HEADER]: OPENGENI_API_CONTRACT_REVISION,
|
|
643
|
+
"x-opengeni-site-id": site.id,
|
|
644
|
+
});
|
|
645
|
+
if (site.versionId) headers.set("x-opengeni-site-version", site.versionId);
|
|
646
|
+
if (input.body !== undefined) headers.set("content-type", "application/json");
|
|
647
|
+
return new Request(new URL(input.path, input.origin), {
|
|
648
|
+
method: input.method,
|
|
649
|
+
headers,
|
|
650
|
+
...(input.body === undefined ? {} : { body: JSON.stringify(input.body) }),
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** Conversations cancelled per stop (newest activity first); the rest end on their own caps. */
|
|
655
|
+
const STOP_CONVERSATIONS_LIMIT = 200;
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Stop a Site's viewers after a takedown, unpublish or narrower access:
|
|
659
|
+
* revoke every site-session now, and after commit cancel the Site's agent
|
|
660
|
+
* conversations that may still be working (in-flight turns and queued
|
|
661
|
+
* messages), as the Site itself through the ordinary control route. The
|
|
662
|
+
* gateway refuses new work at once regardless: every site-runtime call
|
|
663
|
+
* re-checks the publication. Without an in-process dispatch (the operator
|
|
664
|
+
* script) the cancellation is skipped and logged; per-conversation caps
|
|
665
|
+
* still bound what running turns can spend.
|
|
666
|
+
*/
|
|
667
|
+
export async function stopSiteViewers(
|
|
668
|
+
deps: SitePublishingDeps,
|
|
669
|
+
scope: SitePublishingScope,
|
|
670
|
+
siteId: string,
|
|
671
|
+
options: {
|
|
672
|
+
revokeSessions?: boolean;
|
|
673
|
+
cancelConversations?: boolean;
|
|
674
|
+
/** Cancel only conversations holding tools outside these (see `siteAgentToolsNarrowed`). */
|
|
675
|
+
toolsNarrowedTo?: { tools: readonly string[]; everyoneTools: boolean };
|
|
676
|
+
reason: string;
|
|
677
|
+
},
|
|
678
|
+
): Promise<{ revokedSessions: number }> {
|
|
679
|
+
let revokedSessions = 0;
|
|
680
|
+
if (options.revokeSessions !== false) {
|
|
681
|
+
try {
|
|
682
|
+
revokedSessions = await revokeSiteViewerSessions(deps.db, scope, siteId);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
console.warn("[site-publishing] could not revoke viewer sessions", {
|
|
685
|
+
siteId,
|
|
686
|
+
error: error instanceof Error ? error.message : String(error),
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (options.cancelConversations === false) return { revokedSessions };
|
|
691
|
+
const dispatch = sitePublishingDispatch(deps.settings);
|
|
692
|
+
if (!dispatch) {
|
|
693
|
+
console.warn("[site-publishing] conversation cancel skipped: no in-process dispatch", {
|
|
694
|
+
siteId,
|
|
695
|
+
});
|
|
696
|
+
return { revokedSessions };
|
|
697
|
+
}
|
|
698
|
+
const effects = sitePublishingEffects(deps.settings);
|
|
699
|
+
const origin = deps.settings.publicBaseUrl ?? "http://localhost";
|
|
700
|
+
effects.schedule("cancel conversations", async () => {
|
|
701
|
+
const sessionIds = await listActiveSiteRuntimeConversations(deps.db, scope, {
|
|
702
|
+
siteId,
|
|
703
|
+
limit: STOP_CONVERSATIONS_LIMIT,
|
|
704
|
+
...(options.toolsNarrowedTo ? { toolsNarrowedTo: options.toolsNarrowedTo } : {}),
|
|
705
|
+
});
|
|
706
|
+
if (sessionIds.length === 0) return;
|
|
707
|
+
// The Site acts with its own origin (a Site and one of its versions).
|
|
708
|
+
const versionId = (await getPublishedSite(deps.db, scope, siteId))?.currentVersionId ?? null;
|
|
709
|
+
if (!versionId) {
|
|
710
|
+
console.warn("[site-publishing] conversation cancel skipped: the Site has no version", {
|
|
711
|
+
siteId,
|
|
712
|
+
});
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const operation = crypto.randomUUID();
|
|
716
|
+
for (const sessionId of sessionIds) {
|
|
717
|
+
try {
|
|
718
|
+
const response = await dispatch(
|
|
719
|
+
await siteServiceSessionRequest(
|
|
720
|
+
deps,
|
|
721
|
+
scope,
|
|
722
|
+
{ id: siteId, versionId },
|
|
723
|
+
{
|
|
724
|
+
origin,
|
|
725
|
+
method: "POST",
|
|
726
|
+
path: `/v1/workspaces/${scope.workspaceId}/sessions/${sessionId}/control`,
|
|
727
|
+
body: {
|
|
728
|
+
action: "cancel",
|
|
729
|
+
clientEventId: `site-stop:${operation}:${sessionId}`.slice(0, 200),
|
|
730
|
+
reason: options.reason.slice(0, 500),
|
|
731
|
+
},
|
|
732
|
+
label: "Published Site",
|
|
733
|
+
},
|
|
734
|
+
),
|
|
735
|
+
);
|
|
736
|
+
if (!response.ok)
|
|
737
|
+
console.warn("[site-publishing] could not cancel a Site conversation", {
|
|
738
|
+
siteId,
|
|
739
|
+
sessionId,
|
|
740
|
+
status: response.status,
|
|
741
|
+
body: (await response.text().catch(() => "")).slice(0, 300),
|
|
742
|
+
});
|
|
743
|
+
else await response.body?.cancel().catch(() => undefined);
|
|
744
|
+
} catch (error) {
|
|
745
|
+
console.warn("[site-publishing] could not cancel a Site conversation", {
|
|
746
|
+
siteId,
|
|
747
|
+
sessionId,
|
|
748
|
+
error: error instanceof Error ? error.message : String(error),
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
return { revokedSessions };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// --- reaching the platform -----------------------------------------------------------
|
|
757
|
+
|
|
758
|
+
const abuseNoticeHours = new Map<string, number>();
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Tell the platform about an abuse report or a flagged content scan: always a
|
|
762
|
+
* structured log line (`site_abuse_notice`), and, when OPENGENI_SITES_ABUSE_EMAIL
|
|
763
|
+
* is set, an email through the deployment's managed email transport (at most
|
|
764
|
+
* one per Site per hour, best effort, after the request). The operator's
|
|
765
|
+
* cross-workspace queue (`scripts/operator/site-moderation-queue.ts`) lists
|
|
766
|
+
* everything open.
|
|
767
|
+
*/
|
|
768
|
+
export function notifySiteAbuse(
|
|
769
|
+
deps: SitePublishingDeps,
|
|
770
|
+
input: {
|
|
771
|
+
scope: SitePublishingScope;
|
|
772
|
+
siteId: string;
|
|
773
|
+
hostname: string;
|
|
774
|
+
kind: "report" | "flagged_scan";
|
|
775
|
+
detail: string;
|
|
776
|
+
},
|
|
777
|
+
): void {
|
|
778
|
+
console.warn(
|
|
779
|
+
JSON.stringify({
|
|
780
|
+
event: "site_abuse_notice",
|
|
781
|
+
kind: input.kind,
|
|
782
|
+
accountId: input.scope.accountId,
|
|
783
|
+
workspaceId: input.scope.workspaceId,
|
|
784
|
+
siteId: input.siteId,
|
|
785
|
+
hostname: input.hostname,
|
|
786
|
+
detail: input.detail.slice(0, 200),
|
|
787
|
+
}),
|
|
788
|
+
);
|
|
789
|
+
const to = deps.settings.sitesAbuseEmail;
|
|
790
|
+
const transport = deps.managedEmailTransport;
|
|
791
|
+
if (!to || !transport) return;
|
|
792
|
+
const hour = Math.floor(Date.now() / 3_600_000);
|
|
793
|
+
const key = `${input.siteId}:${input.kind}`;
|
|
794
|
+
if (abuseNoticeHours.get(key) === hour) return;
|
|
795
|
+
if (abuseNoticeHours.size > 10_000) abuseNoticeHours.clear();
|
|
796
|
+
abuseNoticeHours.set(key, hour);
|
|
797
|
+
const what = input.kind === "report" ? "An abuse report" : "A flagged content scan";
|
|
798
|
+
const text = [
|
|
799
|
+
`${what} for the published Site https://${input.hostname}/`,
|
|
800
|
+
`Site: ${input.siteId}`,
|
|
801
|
+
`Workspace: ${input.scope.workspaceId}`,
|
|
802
|
+
`Detail: ${input.detail.slice(0, 200)}`,
|
|
803
|
+
"",
|
|
804
|
+
"List open items with scripts/operator/site-moderation-queue.ts; take a Site down with scripts/operator/site-takedown.ts.",
|
|
805
|
+
].join("\n");
|
|
806
|
+
sitePublishingEffects(deps.settings).schedule("abuse notice", async () => {
|
|
807
|
+
const result = await transport.send({
|
|
808
|
+
kind: "site_abuse_notice",
|
|
809
|
+
from: transport.sender,
|
|
810
|
+
to,
|
|
811
|
+
subject: `[Opengeni Sites] ${what}: ${input.hostname}`.slice(0, 200),
|
|
812
|
+
text,
|
|
813
|
+
html: `<pre>${text.replace(/[&<>]/gu, (c) => (c === "&" ? "&" : c === "<" ? "<" : ">"))}</pre>`,
|
|
814
|
+
idempotencyKey: `site-abuse-notice:${key}:${hour}`,
|
|
815
|
+
});
|
|
816
|
+
if (result.status !== "sent")
|
|
817
|
+
console.warn("[site-publishing] abuse notice email not sent", {
|
|
818
|
+
siteId: input.siteId,
|
|
819
|
+
status: result.status,
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// --- management ------------------------------------------------------------------
|
|
825
|
+
|
|
826
|
+
export type SiteManagement = {
|
|
827
|
+
scope: SitePublishingScope;
|
|
828
|
+
site: SiteDataSite;
|
|
829
|
+
record: PublishedSiteRecord;
|
|
830
|
+
manager: boolean;
|
|
831
|
+
admin: boolean;
|
|
832
|
+
owner: boolean;
|
|
833
|
+
subjectId: string | null;
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Publication changes come from people and their own agents, never from a
|
|
838
|
+
* Site page: refuse a request that carries a validated Site origin (a Site or
|
|
839
|
+
* frame page reaching the API through a host) and an agent started from a
|
|
840
|
+
* Site page, or acting in a turn one sent (the Site clamp).
|
|
841
|
+
*/
|
|
842
|
+
export async function requireSitePublicationWriter(
|
|
843
|
+
deps: SitePublishingDeps,
|
|
844
|
+
grant: AccessGrant,
|
|
845
|
+
): Promise<void> {
|
|
846
|
+
if (currentSiteSessionOrigin())
|
|
847
|
+
throw sitePublishingError(
|
|
848
|
+
403,
|
|
849
|
+
"forbidden",
|
|
850
|
+
"site_page_forbidden",
|
|
851
|
+
"A Site page cannot change Site publications",
|
|
852
|
+
);
|
|
853
|
+
try {
|
|
854
|
+
await assertNotSiteClampedAgent(deps.db, grant, "Site publications");
|
|
855
|
+
} catch (error) {
|
|
856
|
+
if (error instanceof HTTPException && error.status === 403)
|
|
857
|
+
throw sitePublishingError(
|
|
858
|
+
403,
|
|
859
|
+
"forbidden",
|
|
860
|
+
"site_agent_forbidden",
|
|
861
|
+
"An agent started from a Site page cannot change Site publications",
|
|
862
|
+
);
|
|
863
|
+
throw error;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
export const SITE_AGENT_OTHER_SITE_MESSAGE =
|
|
868
|
+
"An agent started from a Site page can change only its own Site and Sites it created";
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Whether `grant` may change a Site's content (new versions, rollback,
|
|
872
|
+
* archive, restore, audience). An agent started from a Site page may only
|
|
873
|
+
* for a Site it was started from, or one an agent in its own session tree
|
|
874
|
+
* created with the artifact tools (the Site's first version records that
|
|
875
|
+
* session). It never rewrites another Site the person can edit, published
|
|
876
|
+
* or not, so a page cannot spread itself through the person's drafts. Other
|
|
877
|
+
* callers are not limited here.
|
|
878
|
+
*/
|
|
879
|
+
export async function siteAgentMayChangeArtifact(
|
|
880
|
+
deps: SitePublishingDeps,
|
|
881
|
+
grant: AccessGrant,
|
|
882
|
+
artifactId: string,
|
|
883
|
+
): Promise<boolean> {
|
|
884
|
+
const clamp = await siteAgentClamp(deps.db, grant);
|
|
885
|
+
if (!clamp?.clamped || clamp.originSiteIds.includes(artifactId)) return true;
|
|
886
|
+
const sessionId = grant.metadata?.sessionId;
|
|
887
|
+
if (typeof sessionId !== "string") return false;
|
|
888
|
+
return await artifactCreatedInSessionTree(
|
|
889
|
+
deps.db,
|
|
890
|
+
{ accountId: grant.accountId, workspaceId: grant.workspaceId },
|
|
891
|
+
artifactId,
|
|
892
|
+
sessionId,
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** `siteAgentMayChangeArtifact`, refusing with 403 `site_agent_forbidden`. */
|
|
897
|
+
export async function requireSiteAgentMayChangeArtifact(
|
|
898
|
+
deps: SitePublishingDeps,
|
|
899
|
+
grant: AccessGrant,
|
|
900
|
+
artifactId: string,
|
|
901
|
+
): Promise<void> {
|
|
902
|
+
if (!(await siteAgentMayChangeArtifact(deps, grant, artifactId)))
|
|
903
|
+
throw sitePublishingError(
|
|
904
|
+
403,
|
|
905
|
+
"forbidden",
|
|
906
|
+
"site_agent_forbidden",
|
|
907
|
+
SITE_AGENT_OTHER_SITE_MESSAGE,
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Resolve a Site for publication management. The caller must see the Site
|
|
913
|
+
* (its audience); managers are the Site Data managers (owner, workspace admin,
|
|
914
|
+
* an agent acting for one, or a person-less key with artifacts:publish).
|
|
915
|
+
* `mutate` callers must also pass `requireSitePublicationWriter`.
|
|
916
|
+
*/
|
|
917
|
+
export async function resolveSiteManagement(
|
|
918
|
+
deps: SitePublishingDeps,
|
|
919
|
+
grant: AccessGrant,
|
|
920
|
+
siteId: string,
|
|
921
|
+
options: { mutate?: boolean } = {},
|
|
922
|
+
): Promise<SiteManagement> {
|
|
923
|
+
if (options.mutate) await requireSitePublicationWriter(deps, grant);
|
|
924
|
+
const scope = { accountId: grant.accountId, workspaceId: grant.workspaceId };
|
|
925
|
+
const viewer = await siteViewerForGrant(deps.db, grant);
|
|
926
|
+
const site = await getSiteDataSite(deps.db, scope, viewer, siteId);
|
|
927
|
+
const record = site ? await getPublishedSite(deps.db, scope, siteId) : null;
|
|
928
|
+
if (!site || !record)
|
|
929
|
+
throw sitePublishingError(404, "not_found", "site_not_found", "Site not found");
|
|
930
|
+
const owner = site.ownerSubjectId ?? site.createdBySubjectId ?? null;
|
|
931
|
+
return {
|
|
932
|
+
scope,
|
|
933
|
+
site,
|
|
934
|
+
record,
|
|
935
|
+
manager: siteDataRole(grant, site, viewer.subjectId) === "manager",
|
|
936
|
+
admin: hasLiteralPermission(grant.permissions, "workspace:admin"),
|
|
937
|
+
owner: viewer.subjectId !== null && owner === viewer.subjectId,
|
|
938
|
+
subjectId: viewer.subjectId,
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
export function requireSiteManager(management: SiteManagement): void {
|
|
943
|
+
if (!management.manager)
|
|
944
|
+
throw sitePublishingError(
|
|
945
|
+
403,
|
|
946
|
+
"forbidden",
|
|
947
|
+
"site_manager_required",
|
|
948
|
+
"Only the Site's owner or a workspace admin can manage its publication",
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function randomSuffix(): string {
|
|
953
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
954
|
+
const bytes = randomBytes(4);
|
|
955
|
+
return [...bytes].map((byte) => alphabet[byte % alphabet.length]).join("");
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function normalizeEmails(emails: readonly string[]): string[] {
|
|
959
|
+
return [...new Set(emails.map((email) => email.trim().toLowerCase()).filter(Boolean))].sort();
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
export async function siteLimitInputs(
|
|
963
|
+
deps: SitePublishingDeps,
|
|
964
|
+
scope: SitePublishingScope,
|
|
965
|
+
publication: Pick<SitePublicationRecord, "limitProfile"> | null,
|
|
966
|
+
): Promise<{ site: StoredSiteLimitProfile | null; workspace: StoredSiteLimitProfile | null }> {
|
|
967
|
+
const site = publication?.limitProfile ?? null;
|
|
968
|
+
return { site, workspace: site ? null : await getWorkspaceSiteLimitProfile(deps.db, scope) };
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** The publication as the management API returns it. */
|
|
972
|
+
export async function sitePublicationView(
|
|
973
|
+
deps: SitePublishingDeps,
|
|
974
|
+
scope: SitePublishingScope,
|
|
975
|
+
publication: SitePublicationRecord,
|
|
976
|
+
): Promise<SitePublication> {
|
|
977
|
+
const domain = requireSitesDomain(deps.settings);
|
|
978
|
+
const profiles = await siteLimitInputs(deps, scope, publication);
|
|
979
|
+
const [openReports, usage, readiness] = await Promise.all([
|
|
980
|
+
countOpenSiteAbuseReports(deps.db, scope, publication.siteId),
|
|
981
|
+
readSiteRuntimeUsage(deps.db, scope, {
|
|
982
|
+
siteId: publication.siteId,
|
|
983
|
+
viewerPrincipal: SITE_RUNTIME_SITE_PRINCIPAL,
|
|
984
|
+
}),
|
|
985
|
+
readSitePublicationReadiness(deps, scope, publication),
|
|
986
|
+
]);
|
|
987
|
+
const hostname = siteHostname(publication.label, domain);
|
|
988
|
+
return {
|
|
989
|
+
siteId: publication.siteId,
|
|
990
|
+
workspaceId: publication.workspaceId,
|
|
991
|
+
label: publication.label,
|
|
992
|
+
hostname,
|
|
993
|
+
url: `https://${hostname}/`,
|
|
994
|
+
access: publication.access,
|
|
995
|
+
status: publication.status,
|
|
996
|
+
noindex: publication.noindex || deps.settings.sitesForceNoindex,
|
|
997
|
+
agent: { ...publication.agent, guestTools: false },
|
|
998
|
+
limitProfile: sourceSiteLimitProfile(profiles.site, profiles.workspace),
|
|
999
|
+
effectiveLimits: effectiveSiteLimits(deps.settings, profiles.site, profiles.workspace),
|
|
1000
|
+
invitedEmails: publication.invitedEmails,
|
|
1001
|
+
moderation: {
|
|
1002
|
+
openReports,
|
|
1003
|
+
suspendedAt: publication.suspendedAt?.toISOString() ?? null,
|
|
1004
|
+
suspendedBy: publication.suspendedBy,
|
|
1005
|
+
suspendedReason: publication.suspendedReason,
|
|
1006
|
+
},
|
|
1007
|
+
usage: {
|
|
1008
|
+
agentMicrosThisMonth: usage.siteMonth.agentMicros,
|
|
1009
|
+
agentMicrosToday: usage.siteDay.agentMicros,
|
|
1010
|
+
conversationsToday: usage.siteDay.conversations,
|
|
1011
|
+
},
|
|
1012
|
+
warnings: readiness.warnings,
|
|
1013
|
+
outsideAgentModel: readiness.outsideAgentModel,
|
|
1014
|
+
createdAt: publication.createdAt.toISOString(),
|
|
1015
|
+
updatedAt: publication.updatedAt.toISOString(),
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/** Hostname availability for this Site (the label rules, then global uniqueness and the hold). */
|
|
1020
|
+
export async function siteHostnameAvailability(
|
|
1021
|
+
deps: SitePublishingDeps,
|
|
1022
|
+
scope: SitePublishingScope,
|
|
1023
|
+
label: string,
|
|
1024
|
+
siteId: string | null,
|
|
1025
|
+
): Promise<SiteHostnameAvailability> {
|
|
1026
|
+
const domain = requireSitesDomain(deps.settings);
|
|
1027
|
+
const problem = siteHostnameLabelProblem(label);
|
|
1028
|
+
if (problem)
|
|
1029
|
+
return {
|
|
1030
|
+
label,
|
|
1031
|
+
hostname: null,
|
|
1032
|
+
available: false,
|
|
1033
|
+
problem,
|
|
1034
|
+
message: SITE_HOSTNAME_PROBLEM_MESSAGES[problem],
|
|
1035
|
+
};
|
|
1036
|
+
const check = await checkSiteHostnameClaim(
|
|
1037
|
+
deps.db,
|
|
1038
|
+
scope,
|
|
1039
|
+
label,
|
|
1040
|
+
siteId,
|
|
1041
|
+
deps.settings.sitesHostnameHoldDays,
|
|
1042
|
+
);
|
|
1043
|
+
return check.available
|
|
1044
|
+
? {
|
|
1045
|
+
label,
|
|
1046
|
+
hostname: siteHostname(label, domain),
|
|
1047
|
+
available: true,
|
|
1048
|
+
problem: null,
|
|
1049
|
+
message: null,
|
|
1050
|
+
}
|
|
1051
|
+
: {
|
|
1052
|
+
label,
|
|
1053
|
+
hostname: siteHostname(label, domain),
|
|
1054
|
+
available: false,
|
|
1055
|
+
problem: check.problem,
|
|
1056
|
+
message:
|
|
1057
|
+
check.problem === "held"
|
|
1058
|
+
? "This name was released recently and is held for its previous owner."
|
|
1059
|
+
: check.problem === "reserved"
|
|
1060
|
+
? "This name is reserved."
|
|
1061
|
+
: "This name is already taken.",
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Create or change a Site's publication. The caller is a manager; a Site must
|
|
1067
|
+
* be active and visible to its workspace to be served; public access needs the
|
|
1068
|
+
* deployment to allow it; changing the limit profile needs a workspace admin.
|
|
1069
|
+
*/
|
|
1070
|
+
export async function upsertSitePublicationForManager(
|
|
1071
|
+
deps: SitePublishingDeps,
|
|
1072
|
+
grant: AccessGrant,
|
|
1073
|
+
management: SiteManagement,
|
|
1074
|
+
request: UpsertSitePublicationRequest,
|
|
1075
|
+
): Promise<{ publication: SitePublication; created: boolean }> {
|
|
1076
|
+
requireSitesDomain(deps.settings);
|
|
1077
|
+
requireSiteManager(management);
|
|
1078
|
+
const { scope, record } = management;
|
|
1079
|
+
const existing = await getSitePublication(deps.db, scope, record.id);
|
|
1080
|
+
if (
|
|
1081
|
+
existing?.status === "suspended" &&
|
|
1082
|
+
(request.status !== undefined ||
|
|
1083
|
+
(request.label !== undefined && request.label !== existing.label))
|
|
1084
|
+
)
|
|
1085
|
+
throw sitePublishingError(
|
|
1086
|
+
409,
|
|
1087
|
+
"conflict",
|
|
1088
|
+
"site_suspended",
|
|
1089
|
+
"This Site was taken down; restore it first",
|
|
1090
|
+
);
|
|
1091
|
+
const activating =
|
|
1092
|
+
(request.status ?? existing?.status ?? "active") === "active" &&
|
|
1093
|
+
existing?.status !== "suspended";
|
|
1094
|
+
// The Opengeni-wide platform brake stops new publications (already
|
|
1095
|
+
// published Sites keep serving, and can still be changed or unpublished).
|
|
1096
|
+
// In credits mode, so does a used-up monthly allowance the workspace
|
|
1097
|
+
// cannot pay for (402 site_platform_quota_exhausted).
|
|
1098
|
+
if (activating && existing?.status !== "active") {
|
|
1099
|
+
await requireSitePlatformOpen(deps, "publish");
|
|
1100
|
+
await requireSitePlatformQuota(deps, scope, ["requests", "storage_bytes"]);
|
|
1101
|
+
}
|
|
1102
|
+
if (activating && !siteServable(record))
|
|
1103
|
+
throw sitePublishingError(
|
|
1104
|
+
409,
|
|
1105
|
+
"conflict",
|
|
1106
|
+
record.status !== "active" ? "site_archived" : "site_not_shared",
|
|
1107
|
+
record.status !== "active"
|
|
1108
|
+
? "Restore this Site before publishing it"
|
|
1109
|
+
: "Share this Site with the workspace before publishing it; Only me Sites cannot be published",
|
|
1110
|
+
);
|
|
1111
|
+
if (request.access === "public" && !deps.settings.sitesPublicEnabled)
|
|
1112
|
+
throw sitePublishingError(
|
|
1113
|
+
403,
|
|
1114
|
+
"forbidden",
|
|
1115
|
+
"site_public_disabled",
|
|
1116
|
+
"Public Sites are turned off on this deployment",
|
|
1117
|
+
);
|
|
1118
|
+
if (request.limitProfile !== undefined && !management.admin)
|
|
1119
|
+
throw sitePublishingError(
|
|
1120
|
+
403,
|
|
1121
|
+
"forbidden",
|
|
1122
|
+
"site_admin_required",
|
|
1123
|
+
"Only a workspace admin can change a Site's limits",
|
|
1124
|
+
);
|
|
1125
|
+
// Workspace tools act with the workspace's shared connections for the
|
|
1126
|
+
// workspace members who use the Site's agent (people outside the workspace
|
|
1127
|
+
// never get them): only a workspace admin may give them to the Site's
|
|
1128
|
+
// agent. Anyone who manages the Site may keep or remove them. `guestTools`
|
|
1129
|
+
// is retired: accepted and ignored.
|
|
1130
|
+
const tools = request.agent?.tools;
|
|
1131
|
+
const currentTools = new Set(existing?.agent.tools ?? []);
|
|
1132
|
+
const addsTools = (tools ?? []).some((tool) => !currentTools.has(tool));
|
|
1133
|
+
if (addsTools && !management.admin)
|
|
1134
|
+
throw sitePublishingError(
|
|
1135
|
+
403,
|
|
1136
|
+
"forbidden",
|
|
1137
|
+
"site_admin_required",
|
|
1138
|
+
"Only a workspace admin can give a Site's assistant workspace tools",
|
|
1139
|
+
);
|
|
1140
|
+
for (const tool of tools ?? [])
|
|
1141
|
+
if (!SITE_AGENT_TOOL_ID.test(tool) || RESERVED_SITE_AGENT_TOOL_IDS.has(tool))
|
|
1142
|
+
throw sitePublishingError(
|
|
1143
|
+
400,
|
|
1144
|
+
"validation_failed",
|
|
1145
|
+
"site_tool_invalid",
|
|
1146
|
+
`"${tool.slice(0, 80)}" is not a workspace tool connection a Site's assistant can use`,
|
|
1147
|
+
);
|
|
1148
|
+
const limitProfile =
|
|
1149
|
+
request.limitProfile === undefined
|
|
1150
|
+
? undefined
|
|
1151
|
+
: {
|
|
1152
|
+
kind: storedSiteLimitProfileKind(request.limitProfile.kind),
|
|
1153
|
+
// Overrides adjust any kind's limits (the console edits only the
|
|
1154
|
+
// spend limits and keeps the kind).
|
|
1155
|
+
overrides: request.limitProfile.overrides
|
|
1156
|
+
? { ...request.limitProfile.overrides, __siteLimitsVersion: 2 }
|
|
1157
|
+
: null,
|
|
1158
|
+
};
|
|
1159
|
+
const model = request.agent?.model;
|
|
1160
|
+
if (model) {
|
|
1161
|
+
const profiles = await siteLimitInputs(deps, scope, {
|
|
1162
|
+
limitProfile: limitProfile === undefined ? (existing?.limitProfile ?? null) : limitProfile,
|
|
1163
|
+
});
|
|
1164
|
+
const allowed = effectiveSiteLimits(
|
|
1165
|
+
deps.settings,
|
|
1166
|
+
profiles.site,
|
|
1167
|
+
profiles.workspace,
|
|
1168
|
+
).agentModels;
|
|
1169
|
+
if (allowed && !allowed.includes(model))
|
|
1170
|
+
throw sitePublishingError(
|
|
1171
|
+
400,
|
|
1172
|
+
"validation_failed",
|
|
1173
|
+
"site_model_not_allowed",
|
|
1174
|
+
"This model is not allowed for this Site's agent",
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
const changes = {
|
|
1178
|
+
...(request.label !== undefined ? { label: request.label } : {}),
|
|
1179
|
+
...(request.access !== undefined ? { access: request.access } : {}),
|
|
1180
|
+
...(request.status !== undefined ? { status: request.status } : {}),
|
|
1181
|
+
...(request.noindex !== undefined ? { noindex: request.noindex } : {}),
|
|
1182
|
+
...(request.agent !== undefined
|
|
1183
|
+
? {
|
|
1184
|
+
agent: {
|
|
1185
|
+
...(request.agent.enabled !== undefined ? { enabled: request.agent.enabled } : {}),
|
|
1186
|
+
...(request.agent.model !== undefined ? { model: request.agent.model } : {}),
|
|
1187
|
+
...(request.agent.instructions !== undefined
|
|
1188
|
+
? { instructions: request.agent.instructions }
|
|
1189
|
+
: {}),
|
|
1190
|
+
...(request.agent.tools !== undefined
|
|
1191
|
+
? { tools: request.agent.tools ? [...new Set(request.agent.tools)] : null }
|
|
1192
|
+
: {}),
|
|
1193
|
+
guestTools: false,
|
|
1194
|
+
},
|
|
1195
|
+
}
|
|
1196
|
+
: {}),
|
|
1197
|
+
...(limitProfile !== undefined ? { limitProfile } : {}),
|
|
1198
|
+
...(request.invitedEmails !== undefined
|
|
1199
|
+
? { invitedEmails: normalizeEmails(request.invitedEmails) }
|
|
1200
|
+
: {}),
|
|
1201
|
+
};
|
|
1202
|
+
// A new publication without a label takes a suggested one; a random suffix
|
|
1203
|
+
// collides rarely, so retry a few times.
|
|
1204
|
+
let result: Awaited<ReturnType<typeof upsertSitePublication>> | null = null;
|
|
1205
|
+
for (let attempt = 0; attempt < 5 && result === null; attempt += 1) {
|
|
1206
|
+
try {
|
|
1207
|
+
result = await upsertSitePublication(deps.db, scope, {
|
|
1208
|
+
siteId: record.id,
|
|
1209
|
+
defaultLabel: suggestSiteHostnameLabel(record.title, randomSuffix()),
|
|
1210
|
+
changes,
|
|
1211
|
+
actor: grant.subjectId,
|
|
1212
|
+
holdDays: deps.settings.sitesHostnameHoldDays,
|
|
1213
|
+
defaults: { access: "members", noindex: true },
|
|
1214
|
+
});
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
const retry =
|
|
1217
|
+
error instanceof SitePublishingError &&
|
|
1218
|
+
(error.code === "taken" || error.code === "held") &&
|
|
1219
|
+
request.label === undefined &&
|
|
1220
|
+
existing === null &&
|
|
1221
|
+
attempt < 4;
|
|
1222
|
+
if (!retry) throw error;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
const { publication, created, previousLabel } = result!;
|
|
1226
|
+
const stopping = existing ? siteAccessNarrowed(existing, publication) : null;
|
|
1227
|
+
if (stopping)
|
|
1228
|
+
await stopSiteViewers(deps, scope, record.id, {
|
|
1229
|
+
reason: stopping,
|
|
1230
|
+
revokeSessions: stopping !== "agent_disabled",
|
|
1231
|
+
});
|
|
1232
|
+
else if (existing && siteAgentToolsNarrowed(existing.agent, publication.agent))
|
|
1233
|
+
// Fewer tools, or no longer for everyone: stop the conversations that hold
|
|
1234
|
+
// tools they would not get now. Viewers stay signed in.
|
|
1235
|
+
await stopSiteViewers(deps, scope, record.id, {
|
|
1236
|
+
reason: "agent_tools_narrowed",
|
|
1237
|
+
revokeSessions: false,
|
|
1238
|
+
toolsNarrowedTo: { tools: publication.agent.tools ?? [], everyoneTools: false },
|
|
1239
|
+
});
|
|
1240
|
+
scheduleSiteEdgeSync(deps, [publication.label, previousLabel], {
|
|
1241
|
+
siteIdHint: record.id,
|
|
1242
|
+
purge: stopping === "unpublished",
|
|
1243
|
+
});
|
|
1244
|
+
if (publication.status === "active" && (created || existing?.status !== "active"))
|
|
1245
|
+
scheduleSiteContentScan(deps, scope, record.id, record.currentVersionId);
|
|
1246
|
+
return { publication: await sitePublicationView(deps, scope, publication), created };
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const ACCESS_RANK = { members: 0, invited: 1, public: 2 } as const;
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Why a publication change must stop current viewers, or null: unpublished,
|
|
1253
|
+
* a narrower access mode, a removed invitation, or the assistant turned off
|
|
1254
|
+
* (conversations only).
|
|
1255
|
+
*/
|
|
1256
|
+
export function siteAccessNarrowed(
|
|
1257
|
+
before: Pick<SitePublicationRecord, "status" | "access" | "invitedEmails" | "agent">,
|
|
1258
|
+
after: Pick<SitePublicationRecord, "status" | "access" | "invitedEmails" | "agent">,
|
|
1259
|
+
): "unpublished" | "access_narrowed" | "agent_disabled" | null {
|
|
1260
|
+
if (before.status === "active" && after.status !== "active") return "unpublished";
|
|
1261
|
+
if (ACCESS_RANK[after.access] < ACCESS_RANK[before.access]) return "access_narrowed";
|
|
1262
|
+
if (after.access === "invited") {
|
|
1263
|
+
const kept = new Set(after.invitedEmails);
|
|
1264
|
+
if (before.invitedEmails.some((email) => !kept.has(email))) return "access_narrowed";
|
|
1265
|
+
}
|
|
1266
|
+
if (before.agent.enabled && !after.agent.enabled) return "agent_disabled";
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/** Whether a Site's workspace tools narrowed: a tool removed. */
|
|
1271
|
+
export function siteAgentToolsNarrowed(
|
|
1272
|
+
before: Pick<SitePublicationRecord["agent"], "tools">,
|
|
1273
|
+
after: Pick<SitePublicationRecord["agent"], "tools">,
|
|
1274
|
+
): boolean {
|
|
1275
|
+
const kept = new Set(after.tools ?? []);
|
|
1276
|
+
return (before.tools ?? []).some((tool) => !kept.has(tool));
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/** MCP server ids (the session tool reference format). */
|
|
1280
|
+
const SITE_AGENT_TOOL_ID = /^[A-Za-z0-9_-]{1,200}$/u;
|
|
1281
|
+
/** The first-party server is never a Site agent's tool: it is the workspace itself. */
|
|
1282
|
+
const RESERVED_SITE_AGENT_TOOL_IDS = new Set(["opengeni"]);
|