@opengeni/api-router 0.30.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-grant-rls.d.ts +3 -0
- package/dist/admission-trace.d.ts +8 -0
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.d.ts +50 -2
- package/dist/app.js +12 -1
- package/dist/artifact-catalog-cursor.d.ts +27 -0
- package/dist/auth/managed-auth-attempt-context.d.ts +10 -0
- package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +6 -0
- package/dist/auth/managed-auth.d.ts +102 -3
- package/dist/auth/managed-email.d.ts +29 -0
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/auth/new-signups-gate.d.ts +23 -0
- package/dist/auth/organization-user-setup.d.ts +80 -0
- package/dist/auth/signup-funnel-metrics.d.ts +44 -0
- package/dist/browser-controller-authority.d.ts +1 -61
- package/dist/browser-ephemeral.d.ts +5 -0
- package/dist/browser-state-authority.d.ts +1 -27
- package/dist/chunk-6TZ5LG3O.js +123948 -0
- package/dist/chunk-6TZ5LG3O.js.map +1 -0
- package/dist/chunk-OGQDXVXY.js +18 -0
- package/dist/chunk-OGQDXVXY.js.map +1 -0
- package/dist/claude-subscription-account-conflicts.d.ts +1 -0
- package/dist/claude-subscription-account-usage.d.ts +39 -0
- package/dist/claude-subscription-oauth.d.ts +25 -0
- package/dist/claude-subscription-profile.d.ts +9 -0
- package/dist/claude-subscription-usage.d.ts +44 -0
- package/dist/claude-workspace-connection.d.ts +15 -0
- package/dist/codemode.d.ts +32 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-ownership.d.ts +54 -0
- package/dist/controller-data-plane.d.ts +32 -0
- package/dist/editable-artifact-office-import.d.ts +7 -4
- package/dist/editable-artifact-source-session.d.ts +3 -0
- package/dist/editable-artifact-websocket.d.ts +10 -18
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/fatal-process-boundary.d.ts +25 -0
- package/dist/github-access.d.ts +31 -2
- package/dist/http/acting-person.d.ts +24 -0
- package/dist/http/api-contract-compat.d.ts +19 -0
- package/dist/http/api-error.d.ts +27 -1
- package/dist/http/cors.d.ts +17 -0
- package/dist/http/deprecation.d.ts +42 -0
- package/dist/http/interaction-control-error.d.ts +13 -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/request-body.d.ts +27 -0
- package/dist/http/request-source.d.ts +52 -0
- package/dist/http/sse.d.ts +17 -6
- package/dist/http/unmatched-route.d.ts +18 -0
- package/dist/http/user-content.d.ts +36 -0
- package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +1129 -166
- package/dist/index.js.map +1 -1
- package/dist/integration-endpoint-test.d.ts +20 -0
- package/dist/integrations/api-integrations.d.ts +2 -2
- package/dist/integrations/atlassian.d.ts +29 -3
- package/dist/integrations/connect-authority.d.ts +1 -0
- package/dist/integrations/connect-callback-return.d.ts +6 -0
- package/dist/integrations/fiken.d.ts +16 -2
- package/dist/integrations/github-app-connect.d.ts +47 -0
- package/dist/integrations/github-installation-proof.d.ts +3 -0
- package/dist/integrations/github-lens-connect.d.ts +116 -0
- package/dist/integrations/github-skill-source.d.ts +1 -5
- package/dist/integrations/google-drive.d.ts +25 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/marketplace-plugin.d.ts +2691 -0
- package/dist/integrations/oauth-client.d.ts +178 -18
- package/dist/integrations/oauth-profiles.d.ts +222 -0
- package/dist/integrations/oauth-return-path.d.ts +1 -0
- package/dist/integrations/personal-github-repositories.d.ts +84 -0
- package/dist/integrations/personal-github.d.ts +42 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +19 -1
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +367 -12
- package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
- package/dist/integrations/slack-install.d.ts +20 -0
- package/dist/integrations/slack-interactions.d.ts +113 -10
- package/dist/integrations/slack-mrkdwn.d.ts +41 -0
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/slack-session-defaults.d.ts +58 -0
- package/dist/integrations/slack-task-file-upload.d.ts +36 -0
- package/dist/integrations/social-oauth.d.ts +7 -0
- package/dist/interaction-agent-access.d.ts +15 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/interaction-metrics.d.ts +2 -0
- package/dist/mcp/action-catalog-types.d.ts +9 -0
- package/dist/mcp/action-catalog.gen.d.ts +2 -0
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/child-read-evidence.d.ts +9 -0
- package/dist/mcp/command-read.d.ts +24 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/contract-input.d.ts +11 -0
- package/dist/mcp/documents.d.ts +7 -5
- package/dist/mcp/files.d.ts +2 -2
- package/dist/mcp/first-party-tool-permissions.d.ts +650 -0
- package/dist/mcp/knowledge-entries.d.ts +5 -0
- package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
- package/dist/mcp/receipts.d.ts +9 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-input.d.ts +1189 -0
- package/dist/mcp/scheduled-task-view.d.ts +22 -4
- package/dist/mcp/server.d.ts +255 -3
- package/dist/mcp/session-event-view.d.ts +94 -0
- package/dist/mcp/session-view.d.ts +66 -2
- package/dist/mcp/session-wait.d.ts +174 -0
- package/dist/mcp-oauth.d.ts +52 -0
- package/dist/model-catalog.d.ts +11 -31
- package/dist/observability.d.ts +3 -1
- package/dist/organization-mcp.d.ts +52 -0
- package/dist/organization-recovery-notifications.d.ts +41 -0
- package/dist/routes/analytics-consent.d.ts +26 -0
- package/dist/routes/api-integrations.d.ts +25 -0
- package/dist/routes/api-keys.d.ts +17 -0
- package/dist/routes/artifact-catalog.d.ts +6 -0
- package/dist/routes/automations.d.ts +19 -0
- package/dist/routes/billing.d.ts +49 -0
- package/dist/routes/browser-sessions.d.ts +71 -3
- 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 +146 -0
- package/dist/routes/codex.d.ts +76 -2
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/computer-sessions.d.ts +14 -0
- package/dist/routes/connect.d.ts +5 -0
- package/dist/routes/editable-artifacts.d.ts +2 -0
- package/dist/routes/external-identity-links.d.ts +6 -0
- package/dist/routes/feedback.d.ts +3 -0
- package/dist/routes/files.d.ts +4 -0
- package/dist/routes/github-browser-pages.d.ts +12 -0
- package/dist/routes/github.d.ts +2 -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 +30 -1
- package/dist/routes/knowledge.d.ts +3 -0
- package/dist/routes/managed-auth-session-sets.d.ts +26 -0
- package/dist/routes/managed-onboarding.d.ts +29 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/model-connection-access.d.ts +3 -0
- package/dist/routes/organization-integration-policy.d.ts +3 -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-providers.d.ts +3 -0
- package/dist/routes/organization-recovery.d.ts +19 -0
- package/dist/routes/organization-service-accounts.d.ts +3 -0
- package/dist/routes/organization-sessions.d.ts +26 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/session-artifact-associations.d.ts +4 -0
- package/dist/routes/session-history-imports.d.ts +9 -0
- package/dist/routes/sessions.d.ts +44 -6
- package/dist/routes/skill-content.d.ts +4 -0
- package/dist/routes/skill-install-authority.d.ts +6 -0
- package/dist/routes/subscription-account-pools.d.ts +34 -0
- package/dist/routes/subscription-pool-access.d.ts +16 -0
- package/dist/routes/supergrok.d.ts +6 -1
- package/dist/routes/usage-allowances.d.ts +12 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-artifacts.d.ts +2 -1
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-integrations.d.ts +35 -0
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspace-model-providers.d.ts +4 -0
- package/dist/routes/workspaces.d.ts +29 -0
- package/dist/sandbox/auth-callout.d.ts +8 -0
- package/dist/sandbox/channel-a.d.ts +19 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +9 -1
- package/dist/sandbox/machines.d.ts +5 -2
- package/dist/sandbox/metrics-ingestion.d.ts +28 -20
- package/dist/sandbox/rematerialize.d.ts +2 -0
- package/dist/sandbox/session-attach-variable-sets.d.ts +30 -0
- package/dist/sandbox/viewer.d.ts +26 -1
- package/dist/sandbox-file-artifacts.d.ts +50 -0
- package/dist/scheduled-task-deletion.d.ts +13 -0
- package/dist/session-create-observability.d.ts +5 -0
- package/dist/session-event-view-MPS2OZD3.js +292 -0
- package/dist/session-event-view-MPS2OZD3.js.map +1 -0
- package/dist/site-session-origin.d.ts +13 -0
- package/dist/site-uploads.d.ts +41 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/dist/temporal-schedule-sync.d.ts +26 -0
- package/dist/transcription/providers/xai-subscription.d.ts +1 -0
- package/dist/transcription/service.d.ts +2 -1
- package/dist/work-discovery-observability.d.ts +33 -0
- package/dist/workspace-artifact-content.d.ts +28 -0
- package/dist/workspace-artifact-provenance.d.ts +7 -0
- package/dist/workspace-delete-observability.d.ts +10 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/dist/workspace-state-projection.d.ts +3 -5
- package/dist/workspace-tool-gateway-observability.d.ts +17 -0
- package/dist/workspace-tool-gateway.d.ts +123 -0
- package/dist/workspace-webhook-dispatch.d.ts +26 -0
- package/package.json +23 -19
- package/src/access-grant-rls.ts +75 -0
- package/src/admission-trace.ts +14 -0
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +1974 -256
- package/src/artifact-catalog-cursor.ts +60 -0
- package/src/auth/managed-auth-attempt-context.ts +70 -0
- package/src/auth/managed-auth-rate-limits.ts +252 -0
- package/src/auth/managed-auth-session-adapter.ts +222 -0
- package/src/auth/managed-auth.ts +536 -60
- package/src/auth/managed-email.ts +174 -0
- 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 +307 -0
- package/src/auth/signup-funnel-metrics.ts +164 -0
- package/src/browser-controller-authority.ts +1 -186
- package/src/browser-ephemeral.ts +39 -0
- package/src/browser-state-authority.ts +1 -236
- package/src/claude-subscription-account-conflicts.ts +12 -0
- package/src/claude-subscription-account-usage.ts +48 -0
- package/src/claude-subscription-oauth.ts +362 -0
- package/src/claude-subscription-profile.ts +73 -0
- package/src/claude-subscription-usage.ts +97 -0
- package/src/claude-workspace-connection.ts +82 -0
- package/src/codemode.ts +194 -30
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-ownership.ts +199 -0
- package/src/controller-data-plane.ts +81 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +77 -9
- package/src/editable-artifact-production.ts +23 -7
- package/src/editable-artifact-source-session.ts +123 -0
- package/src/editable-artifact-websocket.ts +27 -21
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/fatal-process-boundary.ts +246 -0
- package/src/gateway-realtime.ts +1 -1
- package/src/github-access.ts +147 -5
- package/src/http/acting-person.ts +71 -0
- package/src/http/api-contract-compat.ts +49 -0
- package/src/http/api-error.ts +115 -1
- package/src/http/auth.ts +48 -0
- package/src/http/cors.ts +67 -0
- package/src/http/deprecation.ts +118 -0
- package/src/http/interaction-control-error.ts +164 -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/request-body.ts +127 -0
- package/src/http/request-source.ts +178 -0
- package/src/http/sse.ts +486 -132
- package/src/http/unmatched-route.ts +60 -0
- package/src/http/user-content.ts +158 -0
- package/src/http/websocket-upgrade-dispatch.ts +29 -0
- package/src/index.ts +157 -52
- package/src/integration-endpoint-test.ts +291 -0
- package/src/integrations/api-integrations.ts +98 -11
- package/src/integrations/atlassian.ts +474 -136
- package/src/integrations/connect-authority.ts +2 -0
- package/src/integrations/connect-callback-return.ts +86 -0
- package/src/integrations/fiken.ts +354 -64
- package/src/integrations/github-app-connect.ts +435 -0
- package/src/integrations/github-installation-proof.ts +60 -0
- package/src/integrations/github-lens-connect.ts +76 -0
- package/src/integrations/github-skill-source.ts +2 -142
- package/src/integrations/google-drive.ts +820 -222
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/marketplace-plugin.ts +48 -0
- package/src/integrations/oauth-client.ts +1356 -459
- package/src/integrations/oauth-profiles.ts +537 -0
- package/src/integrations/oauth-return-path.ts +18 -0
- package/src/integrations/personal-github-repositories.ts +674 -0
- package/src/integrations/personal-github.ts +825 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +250 -22
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +963 -239
- package/src/integrations/slack-file-upload-flow.ts +147 -0
- package/src/integrations/slack-install.ts +83 -0
- package/src/integrations/slack-interactions.ts +3170 -474
- package/src/integrations/slack-mrkdwn.ts +193 -0
- package/src/integrations/slack-routing.ts +337 -0
- package/src/integrations/slack-session-defaults.ts +224 -0
- package/src/integrations/slack-task-file-upload.ts +323 -0
- package/src/integrations/social-oauth.ts +213 -15
- package/src/interaction-agent-access.ts +48 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/interaction-metrics.ts +30 -0
- package/src/mcp/action-catalog-types.ts +9 -0
- package/src/mcp/action-catalog.gen.ts +6696 -0
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/child-read-evidence.ts +19 -0
- package/src/mcp/command-read.ts +109 -0
- package/src/mcp/company-profile-agent-admin.ts +198 -0
- package/src/mcp/contract-input.ts +141 -0
- package/src/mcp/documents.ts +65 -377
- package/src/mcp/editable-artifacts.ts +12 -6
- package/src/mcp/files.ts +80 -38
- package/src/mcp/first-party-tool-permissions.ts +305 -0
- package/src/mcp/knowledge-entries.ts +322 -0
- package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
- package/src/mcp/receipts.ts +34 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-input.ts +76 -0
- package/src/mcp/scheduled-task-view.ts +44 -3
- package/src/mcp/server.ts +3030 -1267
- package/src/mcp/session-event-view.ts +383 -0
- package/src/mcp/session-view.ts +84 -3
- package/src/mcp/session-wait.ts +665 -0
- package/src/mcp-oauth.ts +1123 -0
- package/src/model-catalog.ts +91 -333
- package/src/observability.ts +15 -1
- package/src/organization-mcp.ts +397 -0
- package/src/organization-recovery-notifications.ts +103 -0
- package/src/routes/analytics-consent.ts +123 -0
- package/src/routes/api-integrations.ts +364 -74
- package/src/routes/api-keys.ts +336 -10
- package/src/routes/artifact-catalog.ts +279 -0
- package/src/routes/automations.ts +768 -0
- package/src/routes/billing.ts +461 -46
- package/src/routes/browser-sessions.ts +839 -131
- package/src/routes/canonical-human-identities.ts +90 -14
- package/src/routes/capabilities.ts +107 -6
- package/src/routes/channels.ts +20 -3
- package/src/routes/claude-subscription-accounts.ts +334 -0
- package/src/routes/claude-subscription-oauth.ts +142 -0
- package/src/routes/client-errors.ts +362 -0
- package/src/routes/codex.ts +833 -116
- package/src/routes/company-brain.ts +322 -0
- package/src/routes/company-profile.ts +65 -1
- package/src/routes/computer-sessions.ts +557 -106
- package/src/routes/connect.ts +1938 -0
- package/src/routes/connections.ts +1040 -349
- package/src/routes/documents.ts +488 -287
- package/src/routes/editable-artifacts.ts +79 -9
- package/src/routes/enrollments.ts +135 -39
- package/src/routes/environments.ts +154 -58
- package/src/routes/external-identity-links.ts +201 -0
- package/src/routes/feedback.ts +113 -0
- package/src/routes/files.ts +337 -39
- package/src/routes/github-browser-pages.ts +162 -0
- package/src/routes/github.ts +446 -144
- package/src/routes/insights-response-cache.ts +83 -0
- package/src/routes/insights-usage.ts +345 -0
- package/src/routes/insights.ts +153 -19
- package/src/routes/install.ts +133 -38
- package/src/routes/integration-facets.ts +107 -33
- package/src/routes/interaction-resources.ts +30 -3
- package/src/routes/knowledge.ts +488 -0
- package/src/routes/machines.ts +313 -15
- package/src/routes/managed-auth-session-sets.ts +1218 -0
- package/src/routes/managed-onboarding.ts +337 -0
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/memory-slack-publications.ts +3 -2
- package/src/routes/model-connection-access.ts +208 -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 +178 -0
- package/src/routes/organization-memberships.ts +1019 -27
- package/src/routes/organization-model-providers.ts +293 -0
- package/src/routes/organization-recovery.ts +325 -0
- package/src/routes/organization-service-accounts.ts +128 -0
- package/src/routes/organization-sessions.ts +350 -0
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +370 -0
- package/src/routes/plugins.ts +251 -22
- package/src/routes/pr-review-github.ts +881 -0
- package/src/routes/pr-review.ts +622 -0
- package/src/routes/preference-registry.ts +12 -93
- package/src/routes/rigs.ts +167 -50
- package/src/routes/scheduled-tasks.ts +346 -41
- package/src/routes/session-artifact-associations.ts +87 -0
- package/src/routes/session-history-imports.ts +280 -0
- package/src/routes/sessions.ts +2306 -264
- package/src/routes/skill-content.ts +297 -0
- package/src/routes/skill-install-authority.ts +64 -0
- package/src/routes/skills.ts +62 -9
- package/src/routes/social.ts +46 -18
- package/src/routes/subscription-account-pools.ts +436 -0
- package/src/routes/subscription-pool-access.ts +126 -0
- package/src/routes/supergrok.ts +219 -303
- package/src/routes/transcription-recordings.ts +38 -3
- package/src/routes/transcriptions.ts +4 -0
- package/src/routes/usage-allowances.ts +378 -0
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/video-generation.ts +14 -10
- package/src/routes/workspace-artifacts.ts +275 -69
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-integrations.ts +576 -0
- package/src/routes/workspace-learning.ts +159 -0
- package/src/routes/workspace-model-providers.ts +148 -0
- package/src/routes/workspace-state.ts +38 -25
- package/src/routes/workspaces.ts +1004 -127
- package/src/sandbox/auth-callout.ts +96 -19
- package/src/sandbox/channel-a.ts +323 -24
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +81 -6
- package/src/sandbox/machines.ts +218 -76
- package/src/sandbox/metrics-ingestion.ts +411 -62
- package/src/sandbox/rematerialize.ts +121 -19
- package/src/sandbox/session-attach-variable-sets.ts +131 -0
- package/src/sandbox/viewer.ts +280 -25
- package/src/sandbox-file-artifacts.ts +413 -0
- package/src/scheduled-task-deletion.ts +138 -0
- package/src/session-create-observability.ts +46 -0
- package/src/session-realtime-context.ts +6 -3
- package/src/site-session-origin.ts +48 -0
- package/src/site-uploads.ts +224 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- package/src/temporal-schedule-sync.ts +74 -0
- package/src/transcription/providers/codex-subscription.ts +2 -0
- package/src/transcription/providers/openai.ts +1 -0
- package/src/transcription/providers/xai-subscription.ts +49 -3
- package/src/transcription/segmenter.ts +8 -2
- package/src/transcription/service.ts +43 -6
- package/src/work-discovery-observability.ts +121 -0
- package/src/workspace-artifact-content.ts +144 -0
- package/src/workspace-artifact-provenance.ts +104 -0
- package/src/workspace-delete-observability.ts +75 -0
- package/src/workspace-deletion.ts +73 -0
- package/src/workspace-state-projection.ts +18 -148
- package/src/workspace-tool-gateway-observability.ts +100 -0
- package/src/workspace-tool-gateway.ts +870 -0
- package/src/workspace-webhook-dispatch.ts +190 -0
- package/src/xai-subscription-auth.ts +9 -2
- package/dist/chunk-2PQMSRCB.js +0 -49759
- package/dist/chunk-2PQMSRCB.js.map +0 -1
- package/dist/routes/packs.d.ts +0 -3
- package/src/routes/packs.ts +0 -729
package/src/http/sse.ts
CHANGED
|
@@ -5,23 +5,29 @@ import {
|
|
|
5
5
|
} from "@opengeni/contracts";
|
|
6
6
|
import {
|
|
7
7
|
getWorkspaceInteractionRevisionState,
|
|
8
|
-
|
|
8
|
+
listSessionEventPage,
|
|
9
9
|
listWorkspaceControlEvents,
|
|
10
10
|
type Database,
|
|
11
11
|
} from "@opengeni/db";
|
|
12
12
|
import {
|
|
13
|
+
coalesceSessionEventDeltasWithCoverage,
|
|
13
14
|
formatSessionEventSse,
|
|
14
15
|
formatWorkspaceControlEventSse,
|
|
16
|
+
requireSessionEventDurableFanoutCapability,
|
|
15
17
|
SESSION_EVENT_SSE_FRAME_MAX_BYTES,
|
|
16
18
|
type EventBus,
|
|
17
19
|
} from "@opengeni/events";
|
|
18
20
|
import type { Observability } from "@opengeni/observability";
|
|
21
|
+
import { MANAGED_AUTH_ACTOR_EPOCH_HEADER } from "@opengeni/core/managed-auth-session-sets";
|
|
19
22
|
|
|
20
23
|
const SESSION_REPLAY_PAGE_SIZE = 100;
|
|
24
|
+
const SESSION_REPLAY_PAGE_MAX_BYTES = 8 * 1024 * 1024;
|
|
21
25
|
const WORKSPACE_CONTROL_REPLAY_PAGE_SIZE = 100;
|
|
22
26
|
export const SSE_QUEUED_FRAME_MAX_COUNT = 1;
|
|
23
27
|
export const SSE_WRITE_STALL_TIMEOUT_MS = 30_000;
|
|
24
28
|
export const SSE_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
29
|
+
export const HTTP1_BROWSER_SSE_BATCH_MAX_BYTES = 512 * 1024;
|
|
30
|
+
export const HTTP1_BROWSER_SSE_BATCH_CONTENT_TYPE = "application/vnd.opengeni.sse-batch";
|
|
25
31
|
type SseStreamKind = "session" | "workspace_control" | "workspace_interaction";
|
|
26
32
|
const activeSseStreams: Record<SseStreamKind, number> = {
|
|
27
33
|
session: 0,
|
|
@@ -37,6 +43,7 @@ export type SseDeliveryBoundObservation = {
|
|
|
37
43
|
};
|
|
38
44
|
|
|
39
45
|
export type ByteBoundedSseStreamOptions = {
|
|
46
|
+
connectionLifetimeMs?: number | undefined;
|
|
40
47
|
maxQueuedBytes?: number;
|
|
41
48
|
stallTimeoutMs?: number;
|
|
42
49
|
onStop?: () => void;
|
|
@@ -57,27 +64,36 @@ export type ByteBoundedSseStream = {
|
|
|
57
64
|
* checking `desiredSize` can still accumulate an unbounded server-side queue.
|
|
58
65
|
*
|
|
59
66
|
* One writer is expected per stream. The Web Streams queue holds at most one
|
|
60
|
-
* complete frame
|
|
67
|
+
* complete frame. One oversized frame is delivered intact rather than lost;
|
|
68
|
+
* the byte budget is a batching target, not a content limit. A second write
|
|
61
69
|
* waits for consumer pull only for a bounded interval; cancellation or a stalled
|
|
62
70
|
* reader wakes it and terminates upstream delivery before another durable page is
|
|
63
|
-
* read. One frame is deliberate:
|
|
64
|
-
*
|
|
71
|
+
* read. One frame is deliberate: queued memory is bounded by one event, not
|
|
72
|
+
* by the size of the session, without imposing a lossy per-message limit.
|
|
65
73
|
*/
|
|
66
74
|
export function createByteBoundedSseStream(
|
|
67
75
|
options: ByteBoundedSseStreamOptions = {},
|
|
68
76
|
): ByteBoundedSseStream {
|
|
69
77
|
const maxQueuedBytes = options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES;
|
|
70
78
|
const stallTimeoutMs = options.stallTimeoutMs ?? SSE_WRITE_STALL_TIMEOUT_MS;
|
|
79
|
+
const connectionLifetimeMs = options.connectionLifetimeMs;
|
|
71
80
|
if (!Number.isSafeInteger(maxQueuedBytes) || maxQueuedBytes <= 0) {
|
|
72
81
|
throw new RangeError("SSE byte high-water mark must be a positive safe integer");
|
|
73
82
|
}
|
|
74
83
|
if (!Number.isSafeInteger(stallTimeoutMs) || stallTimeoutMs <= 0) {
|
|
75
84
|
throw new RangeError("SSE write stall timeout must be a positive safe integer");
|
|
76
85
|
}
|
|
86
|
+
if (
|
|
87
|
+
connectionLifetimeMs !== undefined &&
|
|
88
|
+
(!Number.isSafeInteger(connectionLifetimeMs) || connectionLifetimeMs <= 0)
|
|
89
|
+
) {
|
|
90
|
+
throw new RangeError("SSE connection lifetime must be a positive safe integer");
|
|
91
|
+
}
|
|
77
92
|
const encoder = new TextEncoder();
|
|
78
93
|
let controller!: ReadableStreamDefaultController<Uint8Array>;
|
|
79
94
|
let stopped = false;
|
|
80
95
|
let capacityWake: (() => void) | null = null;
|
|
96
|
+
let lifetimeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
81
97
|
let queuedFrames = 0;
|
|
82
98
|
let queuedBytes = 0;
|
|
83
99
|
|
|
@@ -89,6 +105,10 @@ export function createByteBoundedSseStream(
|
|
|
89
105
|
const stop = (settle: () => void) => {
|
|
90
106
|
if (stopped) return;
|
|
91
107
|
stopped = true;
|
|
108
|
+
if (lifetimeTimer !== null) {
|
|
109
|
+
clearTimeout(lifetimeTimer);
|
|
110
|
+
lifetimeTimer = null;
|
|
111
|
+
}
|
|
92
112
|
wakeWriter();
|
|
93
113
|
options.onStop?.();
|
|
94
114
|
try {
|
|
@@ -123,29 +143,19 @@ export function createByteBoundedSseStream(
|
|
|
123
143
|
size: () => 1,
|
|
124
144
|
},
|
|
125
145
|
);
|
|
146
|
+
if (connectionLifetimeMs !== undefined) {
|
|
147
|
+
lifetimeTimer = setTimeout(() => stop(() => controller.close()), connectionLifetimeMs);
|
|
148
|
+
}
|
|
126
149
|
|
|
127
150
|
return {
|
|
128
151
|
stream,
|
|
129
152
|
write: async (frame) => {
|
|
130
|
-
const chunk = encoder.encode(frame);
|
|
131
|
-
if (chunk.byteLength > maxQueuedBytes) {
|
|
132
|
-
const error = new RangeError(
|
|
133
|
-
`SSE frame cannot fit in the configured queue (${chunk.byteLength} > ${maxQueuedBytes} bytes)`,
|
|
134
|
-
);
|
|
135
|
-
options.onObservation?.({
|
|
136
|
-
reason: "frame_too_large",
|
|
137
|
-
desiredSize: controller.desiredSize,
|
|
138
|
-
queuedFrames,
|
|
139
|
-
queuedBytes,
|
|
140
|
-
});
|
|
141
|
-
stop(() => controller.error(error));
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
153
|
for (;;) {
|
|
145
154
|
if (stopped) return false;
|
|
146
155
|
const desired = controller.desiredSize;
|
|
147
156
|
if (desired === null) return false;
|
|
148
157
|
if (desired >= 1 && queuedFrames === 0) {
|
|
158
|
+
const chunk = encoder.encode(frame);
|
|
149
159
|
controller.enqueue(chunk);
|
|
150
160
|
queuedFrames = 1;
|
|
151
161
|
queuedBytes = chunk.byteLength;
|
|
@@ -267,32 +277,42 @@ export async function sseSessionStream(
|
|
|
267
277
|
signal: AbortSignal,
|
|
268
278
|
options: SessionSseDeliveryOptions = {},
|
|
269
279
|
): Promise<Response> {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
280
|
+
if (isHttp1BrowserBatch(options)) {
|
|
281
|
+
const { events } = await listSessionEventPage(db, workspaceId, sessionId, {
|
|
282
|
+
after,
|
|
283
|
+
limit: SESSION_REPLAY_PAGE_SIZE,
|
|
284
|
+
maxBytes: options.finiteResponseMaxBytes ?? HTTP1_BROWSER_SSE_BATCH_MAX_BYTES,
|
|
285
|
+
});
|
|
286
|
+
await options.reauthorize?.();
|
|
287
|
+
const compactProjection = coalesceSessionEventDeltasWithCoverage(events);
|
|
288
|
+
return finiteSseBatchResponse(
|
|
289
|
+
compactProjection.events.map((event) =>
|
|
290
|
+
formatSessionEventSse(
|
|
291
|
+
event,
|
|
292
|
+
compactProjection.coveredThroughBySequence.get(event.sequence) ?? event.sequence,
|
|
293
|
+
),
|
|
294
|
+
),
|
|
295
|
+
options,
|
|
296
|
+
);
|
|
279
297
|
}
|
|
298
|
+
const durableFanout = requireSessionEventDurableFanoutCapability(bus);
|
|
299
|
+
const heartbeatIntervalMs = resolveHeartbeatInterval(options.heartbeatIntervalMs);
|
|
280
300
|
let lastSent = after;
|
|
281
301
|
let bootstrapping = true;
|
|
282
302
|
let newestBuffered: SessionEvent | null = null;
|
|
283
303
|
let unsubscribe: (() => void) | null = null;
|
|
284
304
|
let delivery: LatestWinsDelivery<SessionEvent> | null = null;
|
|
285
|
-
let
|
|
305
|
+
let stopReconnectObservation = () => {};
|
|
306
|
+
let stopReauthorization = () => {};
|
|
286
307
|
let heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
|
|
287
308
|
let detachAbortListener = () => {};
|
|
288
309
|
let closeMetrics = () => {};
|
|
289
310
|
const stopUpstream = () => {
|
|
290
311
|
closeMetrics();
|
|
291
312
|
detachAbortListener();
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
313
|
+
stopReconnectObservation();
|
|
314
|
+
stopReconnectObservation = () => {};
|
|
315
|
+
stopReauthorization();
|
|
296
316
|
if (heartbeatTimer) {
|
|
297
317
|
clearTimeout(heartbeatTimer);
|
|
298
318
|
heartbeatTimer = null;
|
|
@@ -303,6 +323,7 @@ export async function sseSessionStream(
|
|
|
303
323
|
release?.();
|
|
304
324
|
};
|
|
305
325
|
const channel = createByteBoundedSseStream({
|
|
326
|
+
connectionLifetimeMs: options.connectionLifetimeMs,
|
|
306
327
|
maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
|
|
307
328
|
...(options.stallTimeoutMs === undefined ? {} : { stallTimeoutMs: options.stallTimeoutMs }),
|
|
308
329
|
onObservation: sseObservationReporter("session", options),
|
|
@@ -313,25 +334,111 @@ export async function sseSessionStream(
|
|
|
313
334
|
const fail = (error: unknown) => {
|
|
314
335
|
channel.fail(retryableSseFailure("session event stream delivery failed", error));
|
|
315
336
|
};
|
|
316
|
-
|
|
317
|
-
if (!options.reauthorize || channel.stopped()) return;
|
|
318
|
-
const interval = options.reauthorizeAfterMs ?? 15_000;
|
|
319
|
-
reauthorizationTimer = setTimeout(() => {
|
|
320
|
-
reauthorizationTimer = null;
|
|
321
|
-
void options.reauthorize!()
|
|
322
|
-
.then(scheduleReauthorization)
|
|
323
|
-
.catch((error) => fail(error));
|
|
324
|
-
}, interval);
|
|
325
|
-
};
|
|
326
|
-
scheduleReauthorization();
|
|
337
|
+
stopReauthorization = startSseReauthorization(options, channel.stopped, channel.close);
|
|
327
338
|
let writeTail = Promise.resolve();
|
|
328
339
|
const writeFrame = (frame: string): Promise<void> => {
|
|
329
340
|
const write = writeTail.then(async () => {
|
|
341
|
+
// A periodic check closes an idle stream, while this exact pre-delivery
|
|
342
|
+
// check prevents a buffered/replayed event from crossing a revocation
|
|
343
|
+
// boundary merely because its timer has not fired yet.
|
|
344
|
+
await reauthorizeSseOrClose(options, channel);
|
|
330
345
|
if (!(await channel.write(frame))) throw new SseStreamStoppedError();
|
|
331
346
|
});
|
|
332
347
|
writeTail = write.catch(() => {});
|
|
333
348
|
return write;
|
|
334
349
|
};
|
|
350
|
+
const deliverDurableThrough = async (targetSequence?: number) => {
|
|
351
|
+
while (true) {
|
|
352
|
+
if (targetSequence !== undefined && lastSent >= targetSequence) return;
|
|
353
|
+
const previousLastSent = lastSent;
|
|
354
|
+
const limit =
|
|
355
|
+
targetSequence === undefined
|
|
356
|
+
? SESSION_REPLAY_PAGE_SIZE
|
|
357
|
+
: Math.min(SESSION_REPLAY_PAGE_SIZE, targetSequence - lastSent);
|
|
358
|
+
const replayPage = await listSessionEventPage(db, workspaceId, sessionId, {
|
|
359
|
+
after: lastSent,
|
|
360
|
+
limit,
|
|
361
|
+
maxBytes: SESSION_REPLAY_PAGE_MAX_BYTES,
|
|
362
|
+
});
|
|
363
|
+
const page = replayPage.events;
|
|
364
|
+
const eligible =
|
|
365
|
+
targetSequence === undefined
|
|
366
|
+
? page
|
|
367
|
+
: page.filter((event) => event.sequence <= targetSequence);
|
|
368
|
+
if (eligible.length === 0) {
|
|
369
|
+
if (targetSequence === undefined) return;
|
|
370
|
+
throw new Error(
|
|
371
|
+
`Session event replay stalled before sequence ${targetSequence}; last sent ${lastSent}`,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
// The durable audit log remains exact. The browser transport combines
|
|
375
|
+
// adjacent text deltas into lossless batches carrying `coalescedUntil`, so
|
|
376
|
+
// a long answer cannot create thousands of React renders and starve
|
|
377
|
+
// command acknowledgements behind its own token stream.
|
|
378
|
+
const compactProjection = coalesceSessionEventDeltasWithCoverage(eligible);
|
|
379
|
+
for (const projected of compactProjection.events) {
|
|
380
|
+
const coveredThrough =
|
|
381
|
+
compactProjection.coveredThroughBySequence.get(projected.sequence) ?? projected.sequence;
|
|
382
|
+
await writeFrame(formatSessionEventSse(projected, coveredThrough));
|
|
383
|
+
lastSent = coveredThrough;
|
|
384
|
+
}
|
|
385
|
+
if (lastSent <= previousLastSent) {
|
|
386
|
+
throw new Error(`Session event replay made no progress after sequence ${lastSent}`);
|
|
387
|
+
}
|
|
388
|
+
if (targetSequence !== undefined && lastSent >= targetSequence) return;
|
|
389
|
+
if (targetSequence === undefined && !replayPage.hasMore) return;
|
|
390
|
+
// A byte-selected page may contain fewer rows than requested, especially
|
|
391
|
+
// when one large message travels alone. Use explicit continuation rather
|
|
392
|
+
// than page length; live publication covers appends after the snapshot.
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
let durableDeliveryTail = Promise.resolve();
|
|
396
|
+
const reconcileDurableThrough = (targetSequence?: number): Promise<void> => {
|
|
397
|
+
const deliveryRun = durableDeliveryTail.then(() => deliverDurableThrough(targetSequence));
|
|
398
|
+
durableDeliveryTail = deliveryRun.catch(() => {});
|
|
399
|
+
return deliveryRun;
|
|
400
|
+
};
|
|
401
|
+
let newestReconnectGeneration = 0;
|
|
402
|
+
let reconnectReconcilePending = false;
|
|
403
|
+
let reconnectReconcileRunning = false;
|
|
404
|
+
const drainReconnectReconciliation = () => {
|
|
405
|
+
if (
|
|
406
|
+
bootstrapping ||
|
|
407
|
+
reconnectReconcileRunning ||
|
|
408
|
+
!reconnectReconcilePending ||
|
|
409
|
+
channel.stopped()
|
|
410
|
+
) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
reconnectReconcileRunning = true;
|
|
414
|
+
void (async () => {
|
|
415
|
+
while (reconnectReconcilePending && !channel.stopped()) {
|
|
416
|
+
// Multiple reconnects during one durable read collapse into one newest
|
|
417
|
+
// catch-up. Postgres is authoritative, so that later read covers every
|
|
418
|
+
// disconnect window without one query per heartbeat or buffered event.
|
|
419
|
+
reconnectReconcilePending = false;
|
|
420
|
+
await reconcileDurableThrough();
|
|
421
|
+
}
|
|
422
|
+
})()
|
|
423
|
+
.catch((error) => {
|
|
424
|
+
if (!(error instanceof SseStreamStoppedError)) fail(error);
|
|
425
|
+
})
|
|
426
|
+
.finally(() => {
|
|
427
|
+
reconnectReconcileRunning = false;
|
|
428
|
+
drainReconnectReconciliation();
|
|
429
|
+
});
|
|
430
|
+
};
|
|
431
|
+
const scheduleReconnectReconciliation = (generation: number) => {
|
|
432
|
+
if (generation <= newestReconnectGeneration || channel.stopped()) return;
|
|
433
|
+
newestReconnectGeneration = generation;
|
|
434
|
+
reconnectReconcilePending = true;
|
|
435
|
+
drainReconnectReconciliation();
|
|
436
|
+
};
|
|
437
|
+
const send = async (event: SessionEvent) => {
|
|
438
|
+
const targetSequence = event.sequence;
|
|
439
|
+
if (targetSequence <= lastSent) return;
|
|
440
|
+
await reconcileDurableThrough(targetSequence);
|
|
441
|
+
};
|
|
335
442
|
const scheduleHeartbeat = () => {
|
|
336
443
|
if (channel.stopped()) return;
|
|
337
444
|
heartbeatTimer = setTimeout(() => {
|
|
@@ -343,66 +450,43 @@ export async function sseSessionStream(
|
|
|
343
450
|
});
|
|
344
451
|
}, heartbeatIntervalMs);
|
|
345
452
|
};
|
|
346
|
-
const send = async (event: SessionEvent) => {
|
|
347
|
-
if (event.sequence <= lastSent) return;
|
|
348
|
-
if (event.sequence > lastSent + 1) {
|
|
349
|
-
while (lastSent + 1 < event.sequence) {
|
|
350
|
-
const previousLastSent = lastSent;
|
|
351
|
-
const missing = await listSessionEvents(db, workspaceId, sessionId, {
|
|
352
|
-
after: lastSent,
|
|
353
|
-
limit: Math.min(SESSION_REPLAY_PAGE_SIZE, event.sequence - lastSent - 1),
|
|
354
|
-
});
|
|
355
|
-
if (missing.length === 0) {
|
|
356
|
-
throw new Error(
|
|
357
|
-
`Session event replay stalled before sequence ${event.sequence}; last sent ${lastSent}`,
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
for (const missed of missing) {
|
|
361
|
-
if (missed.sequence >= event.sequence) break;
|
|
362
|
-
if (missed.sequence > lastSent) {
|
|
363
|
-
await writeFrame(formatSessionEventSse(missed));
|
|
364
|
-
lastSent = missed.sequence;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
if (lastSent === previousLastSent) {
|
|
368
|
-
throw new Error(
|
|
369
|
-
`Session event replay made no progress before sequence ${event.sequence}; last sent ${lastSent}`,
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
await writeFrame(formatSessionEventSse(event));
|
|
375
|
-
lastSent = event.sequence;
|
|
376
|
-
};
|
|
377
453
|
delivery = createLatestWinsDelivery(send, fail);
|
|
454
|
+
stopReconnectObservation = durableFanout.subscribeRecovery(scheduleReconnectReconciliation);
|
|
378
455
|
|
|
379
456
|
void (async () => {
|
|
380
|
-
const release = await bus.subscribe(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
457
|
+
const release = await bus.subscribe(
|
|
458
|
+
workspaceId,
|
|
459
|
+
sessionId,
|
|
460
|
+
(events) => {
|
|
461
|
+
if (bootstrapping) {
|
|
462
|
+
for (const event of events) {
|
|
463
|
+
if (!newestBuffered || event.sequence > newestBuffered.sequence) {
|
|
464
|
+
newestBuffered = event;
|
|
465
|
+
}
|
|
385
466
|
}
|
|
467
|
+
} else {
|
|
468
|
+
delivery?.publish(events);
|
|
386
469
|
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
// Live fanout stopped while this stream still held it. Heartbeats alone
|
|
473
|
+
// would keep a silently stale timeline open; fail retryably so the
|
|
474
|
+
// client reconnects and replays from the durable Postgres cursor.
|
|
475
|
+
onTerminated: (error) =>
|
|
476
|
+
fail(retryableSseFailure("session live fanout subscription ended", error)),
|
|
477
|
+
},
|
|
478
|
+
);
|
|
391
479
|
if (channel.stopped()) {
|
|
392
480
|
release();
|
|
393
481
|
return;
|
|
394
482
|
}
|
|
395
483
|
unsubscribe = release;
|
|
396
484
|
|
|
397
|
-
await
|
|
398
|
-
(cursor, limit) => listSessionEvents(db, workspaceId, sessionId, cursor, limit),
|
|
399
|
-
send,
|
|
400
|
-
after,
|
|
401
|
-
SESSION_REPLAY_PAGE_SIZE,
|
|
402
|
-
);
|
|
485
|
+
await reconcileDurableThrough();
|
|
403
486
|
await writeFrame(": connected\n\n");
|
|
404
487
|
scheduleHeartbeat();
|
|
405
488
|
bootstrapping = false;
|
|
489
|
+
drainReconnectReconciliation();
|
|
406
490
|
const buffered = newestBuffered;
|
|
407
491
|
newestBuffered = null;
|
|
408
492
|
if (buffered) delivery.publish([buffered]);
|
|
@@ -419,13 +503,7 @@ export async function sseSessionStream(
|
|
|
419
503
|
detachAbortListener = () => signal.removeEventListener("abort", abort);
|
|
420
504
|
}
|
|
421
505
|
|
|
422
|
-
return
|
|
423
|
-
headers: {
|
|
424
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
425
|
-
"Cache-Control": "no-cache, no-transform",
|
|
426
|
-
Connection: "keep-alive",
|
|
427
|
-
},
|
|
428
|
-
});
|
|
506
|
+
return await sseHttpResponse(channel.stream, options);
|
|
429
507
|
}
|
|
430
508
|
|
|
431
509
|
export async function replaySessionEvents(
|
|
@@ -465,18 +543,35 @@ export async function sseWorkspaceControlStream(
|
|
|
465
543
|
signal: AbortSignal,
|
|
466
544
|
options: SseDeliveryOptions = {},
|
|
467
545
|
): Promise<Response> {
|
|
546
|
+
if (isHttp1BrowserBatch(options)) {
|
|
547
|
+
const events = await listWorkspaceControlEvents(
|
|
548
|
+
db,
|
|
549
|
+
workspaceId,
|
|
550
|
+
after,
|
|
551
|
+
WORKSPACE_CONTROL_REPLAY_PAGE_SIZE,
|
|
552
|
+
);
|
|
553
|
+
await options.reauthorize?.();
|
|
554
|
+
return finiteSseBatchResponse(
|
|
555
|
+
events
|
|
556
|
+
.sort((left, right) => left.sequence - right.sequence)
|
|
557
|
+
.map(formatWorkspaceControlEventSse),
|
|
558
|
+
options,
|
|
559
|
+
);
|
|
560
|
+
}
|
|
468
561
|
const heartbeatIntervalMs = resolveHeartbeatInterval(options.heartbeatIntervalMs);
|
|
469
562
|
let lastSent = after;
|
|
470
563
|
let bootstrapping = true;
|
|
471
564
|
let newestBuffered: WorkspaceControlEvent | null = null;
|
|
472
565
|
let unsubscribe: (() => void) | null = null;
|
|
473
566
|
let delivery: LatestWinsDelivery<WorkspaceControlEvent> | null = null;
|
|
567
|
+
let stopReauthorization = () => {};
|
|
474
568
|
let heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
|
|
475
569
|
let detachAbortListener = () => {};
|
|
476
570
|
let closeMetrics = () => {};
|
|
477
571
|
const stopUpstream = () => {
|
|
478
572
|
closeMetrics();
|
|
479
573
|
detachAbortListener();
|
|
574
|
+
stopReauthorization();
|
|
480
575
|
if (heartbeatTimer) {
|
|
481
576
|
clearTimeout(heartbeatTimer);
|
|
482
577
|
heartbeatTimer = null;
|
|
@@ -487,6 +582,7 @@ export async function sseWorkspaceControlStream(
|
|
|
487
582
|
release?.();
|
|
488
583
|
};
|
|
489
584
|
const channel = createByteBoundedSseStream({
|
|
585
|
+
connectionLifetimeMs: options.connectionLifetimeMs,
|
|
490
586
|
maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
|
|
491
587
|
...(options.stallTimeoutMs === undefined ? {} : { stallTimeoutMs: options.stallTimeoutMs }),
|
|
492
588
|
onObservation: sseObservationReporter("workspace_control", options),
|
|
@@ -497,9 +593,11 @@ export async function sseWorkspaceControlStream(
|
|
|
497
593
|
const fail = (error: unknown) => {
|
|
498
594
|
channel.fail(retryableSseFailure("workspace control stream delivery failed", error));
|
|
499
595
|
};
|
|
596
|
+
stopReauthorization = startSseReauthorization(options, channel.stopped, channel.close);
|
|
500
597
|
let writeTail = Promise.resolve();
|
|
501
598
|
const writeFrame = (frame: string): Promise<void> => {
|
|
502
599
|
const write = writeTail.then(async () => {
|
|
600
|
+
await reauthorizeSseOrClose(options, channel);
|
|
503
601
|
if (!(await channel.write(frame))) throw new SseStreamStoppedError();
|
|
504
602
|
});
|
|
505
603
|
writeTail = write.catch(() => {});
|
|
@@ -551,13 +649,22 @@ export async function sseWorkspaceControlStream(
|
|
|
551
649
|
delivery = createLatestWinsDelivery(send, fail);
|
|
552
650
|
|
|
553
651
|
void (async () => {
|
|
554
|
-
const release = await bus.subscribeWorkspaceControl(
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
652
|
+
const release = await bus.subscribeWorkspaceControl(
|
|
653
|
+
workspaceId,
|
|
654
|
+
(event) => {
|
|
655
|
+
if (bootstrapping) {
|
|
656
|
+
if (!newestBuffered || event.sequence > newestBuffered.sequence) newestBuffered = event;
|
|
657
|
+
} else {
|
|
658
|
+
delivery?.publish([event]);
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
// Same contract as session SSE: a dead live subscription must reconnect
|
|
663
|
+
// and replay the durable control cursor, not idle on heartbeats.
|
|
664
|
+
onTerminated: (error) =>
|
|
665
|
+
fail(retryableSseFailure("workspace control live subscription ended", error)),
|
|
666
|
+
},
|
|
667
|
+
);
|
|
561
668
|
if (channel.stopped()) {
|
|
562
669
|
release();
|
|
563
670
|
return;
|
|
@@ -588,13 +695,7 @@ export async function sseWorkspaceControlStream(
|
|
|
588
695
|
detachAbortListener = () => signal.removeEventListener("abort", abort);
|
|
589
696
|
}
|
|
590
697
|
|
|
591
|
-
return
|
|
592
|
-
headers: {
|
|
593
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
594
|
-
"Cache-Control": "no-cache, no-transform",
|
|
595
|
-
Connection: "keep-alive",
|
|
596
|
-
},
|
|
597
|
-
});
|
|
698
|
+
return await sseHttpResponse(channel.stream, options);
|
|
598
699
|
}
|
|
599
700
|
|
|
600
701
|
export type WorkspaceInteractionSseOptions = SseDeliveryOptions & {
|
|
@@ -618,12 +719,42 @@ export async function sseWorkspaceLiveStream(
|
|
|
618
719
|
signal: AbortSignal,
|
|
619
720
|
options: WorkspaceInteractionSseOptions = {},
|
|
620
721
|
): Promise<Response> {
|
|
722
|
+
if (isHttp1BrowserBatch(options)) {
|
|
723
|
+
const [controlEvents, interactionState] = await Promise.all([
|
|
724
|
+
listWorkspaceControlEvents(db, workspaceId, controlAfter, WORKSPACE_CONTROL_REPLAY_PAGE_SIZE),
|
|
725
|
+
getWorkspaceInteractionRevisionState(db, { accountId, workspaceId }),
|
|
726
|
+
]);
|
|
727
|
+
await options.reauthorize?.();
|
|
728
|
+
const frames: string[] = [];
|
|
729
|
+
if (interactionState.revision > interactionAfter) {
|
|
730
|
+
frames.push(
|
|
731
|
+
formatWorkspaceInteractionRevisionSse(
|
|
732
|
+
WorkspaceInteractionRevisionEvent.parse({
|
|
733
|
+
workspaceId,
|
|
734
|
+
sequence: interactionState.revision,
|
|
735
|
+
revision: interactionState.revision,
|
|
736
|
+
type: "workspace.interaction.changed",
|
|
737
|
+
occurredAt: (interactionState.updatedAt ?? new Date()).toISOString(),
|
|
738
|
+
}),
|
|
739
|
+
),
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
frames.push(
|
|
743
|
+
...controlEvents
|
|
744
|
+
.sort((left, right) => left.sequence - right.sequence)
|
|
745
|
+
.map(formatWorkspaceControlEventSse),
|
|
746
|
+
);
|
|
747
|
+
return finiteSseBatchResponse(frames, options);
|
|
748
|
+
}
|
|
621
749
|
const upstream = new AbortController();
|
|
750
|
+
let stopReauthorization = () => {};
|
|
622
751
|
const channel = createByteBoundedSseStream({
|
|
752
|
+
connectionLifetimeMs: options.connectionLifetimeMs,
|
|
623
753
|
maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
|
|
624
754
|
...(options.stallTimeoutMs === undefined ? {} : { stallTimeoutMs: options.stallTimeoutMs }),
|
|
625
755
|
onObservation: sseObservationReporter("workspace_interaction", options),
|
|
626
756
|
onStop: () => {
|
|
757
|
+
stopReauthorization();
|
|
627
758
|
signal.removeEventListener("abort", abort);
|
|
628
759
|
upstream.abort();
|
|
629
760
|
},
|
|
@@ -634,10 +765,23 @@ export async function sseWorkspaceLiveStream(
|
|
|
634
765
|
};
|
|
635
766
|
if (signal.aborted) abort();
|
|
636
767
|
else signal.addEventListener("abort", abort, { once: true });
|
|
768
|
+
stopReauthorization = startSseReauthorization(options, channel.stopped, channel.close);
|
|
769
|
+
|
|
770
|
+
const upstreamOptions: WorkspaceInteractionSseOptions = {
|
|
771
|
+
...options,
|
|
772
|
+
connectionLifetimeMs: undefined,
|
|
773
|
+
finiteResponseMaxBytes: undefined,
|
|
774
|
+
finiteResponseMediaType: undefined,
|
|
775
|
+
reauthorize: undefined,
|
|
776
|
+
reauthorizeAfterMs: undefined,
|
|
777
|
+
};
|
|
637
778
|
|
|
638
779
|
let writeTail = Promise.resolve(true);
|
|
639
780
|
const write = (frame: string): Promise<boolean> => {
|
|
640
|
-
const pending = writeTail.then(() =>
|
|
781
|
+
const pending = writeTail.then(async () => {
|
|
782
|
+
await reauthorizeSseOrClose(options, channel);
|
|
783
|
+
return await channel.write(frame);
|
|
784
|
+
});
|
|
641
785
|
writeTail = pending.catch(() => false);
|
|
642
786
|
return pending;
|
|
643
787
|
};
|
|
@@ -665,14 +809,21 @@ export async function sseWorkspaceLiveStream(
|
|
|
665
809
|
void (async () => {
|
|
666
810
|
try {
|
|
667
811
|
const [control, interaction] = await Promise.all([
|
|
668
|
-
sseWorkspaceControlStream(
|
|
812
|
+
sseWorkspaceControlStream(
|
|
813
|
+
db,
|
|
814
|
+
bus,
|
|
815
|
+
workspaceId,
|
|
816
|
+
controlAfter,
|
|
817
|
+
upstream.signal,
|
|
818
|
+
upstreamOptions,
|
|
819
|
+
),
|
|
669
820
|
sseWorkspaceInteractionRevisionStream(
|
|
670
821
|
db,
|
|
671
822
|
accountId,
|
|
672
823
|
workspaceId,
|
|
673
824
|
interactionAfter,
|
|
674
825
|
upstream.signal,
|
|
675
|
-
|
|
826
|
+
upstreamOptions,
|
|
676
827
|
),
|
|
677
828
|
]);
|
|
678
829
|
const pumps = [pump(control), pump(interaction)];
|
|
@@ -694,13 +845,7 @@ export async function sseWorkspaceLiveStream(
|
|
|
694
845
|
}
|
|
695
846
|
})();
|
|
696
847
|
|
|
697
|
-
return
|
|
698
|
-
headers: {
|
|
699
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
700
|
-
"Cache-Control": "no-cache, no-transform",
|
|
701
|
-
Connection: "keep-alive",
|
|
702
|
-
},
|
|
703
|
-
});
|
|
848
|
+
return await sseHttpResponse(channel.stream, options);
|
|
704
849
|
}
|
|
705
850
|
|
|
706
851
|
/**
|
|
@@ -716,26 +861,53 @@ export async function sseWorkspaceInteractionRevisionStream(
|
|
|
716
861
|
signal: AbortSignal,
|
|
717
862
|
options: WorkspaceInteractionSseOptions = {},
|
|
718
863
|
): Promise<Response> {
|
|
864
|
+
if (isHttp1BrowserBatch(options)) {
|
|
865
|
+
const state = await getWorkspaceInteractionRevisionState(db, {
|
|
866
|
+
accountId,
|
|
867
|
+
workspaceId,
|
|
868
|
+
});
|
|
869
|
+
await options.reauthorize?.();
|
|
870
|
+
const frames =
|
|
871
|
+
state.revision > after
|
|
872
|
+
? [
|
|
873
|
+
formatWorkspaceInteractionRevisionSse(
|
|
874
|
+
WorkspaceInteractionRevisionEvent.parse({
|
|
875
|
+
workspaceId,
|
|
876
|
+
sequence: state.revision,
|
|
877
|
+
revision: state.revision,
|
|
878
|
+
type: "workspace.interaction.changed",
|
|
879
|
+
occurredAt: (state.updatedAt ?? new Date()).toISOString(),
|
|
880
|
+
}),
|
|
881
|
+
),
|
|
882
|
+
]
|
|
883
|
+
: [];
|
|
884
|
+
return finiteSseBatchResponse(frames, options);
|
|
885
|
+
}
|
|
719
886
|
const heartbeatIntervalMs = resolveHeartbeatInterval(options.heartbeatIntervalMs);
|
|
720
887
|
const pollIntervalMs = resolveInteractionPollInterval(options.pollIntervalMs);
|
|
721
888
|
let lastSent = after;
|
|
722
889
|
let lastWriteAt = Date.now();
|
|
723
890
|
let stopRequested = false;
|
|
891
|
+
let stopReauthorization = () => {};
|
|
724
892
|
let detachAbortListener = () => {};
|
|
725
893
|
let closeMetrics = () => {};
|
|
726
894
|
const channel = createByteBoundedSseStream({
|
|
895
|
+
connectionLifetimeMs: options.connectionLifetimeMs,
|
|
727
896
|
maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
|
|
728
897
|
...(options.stallTimeoutMs === undefined ? {} : { stallTimeoutMs: options.stallTimeoutMs }),
|
|
729
898
|
onObservation: sseObservationReporter("workspace_interaction", options),
|
|
730
899
|
onStop: () => {
|
|
731
900
|
stopRequested = true;
|
|
901
|
+
stopReauthorization();
|
|
732
902
|
closeMetrics();
|
|
733
903
|
detachAbortListener();
|
|
734
904
|
},
|
|
735
905
|
});
|
|
736
906
|
closeMetrics = observeSseConnection("workspace_interaction", after, options.observability);
|
|
907
|
+
stopReauthorization = startSseReauthorization(options, channel.stopped, channel.close);
|
|
737
908
|
|
|
738
909
|
const write = async (frame: string): Promise<boolean> => {
|
|
910
|
+
await reauthorizeSseOrClose(options, channel);
|
|
739
911
|
const accepted = await channel.write(frame);
|
|
740
912
|
if (accepted) lastWriteAt = Date.now();
|
|
741
913
|
return accepted;
|
|
@@ -746,7 +918,10 @@ export async function sseWorkspaceInteractionRevisionStream(
|
|
|
746
918
|
if (!(await write(": connected\n\n"))) return;
|
|
747
919
|
for (;;) {
|
|
748
920
|
if (stopRequested || signal.aborted || channel.stopped()) return;
|
|
749
|
-
const state = await getWorkspaceInteractionRevisionState(db, {
|
|
921
|
+
const state = await getWorkspaceInteractionRevisionState(db, {
|
|
922
|
+
accountId,
|
|
923
|
+
workspaceId,
|
|
924
|
+
});
|
|
750
925
|
if (state.revision > lastSent) {
|
|
751
926
|
const event = WorkspaceInteractionRevisionEvent.parse({
|
|
752
927
|
workspaceId,
|
|
@@ -776,13 +951,7 @@ export async function sseWorkspaceInteractionRevisionStream(
|
|
|
776
951
|
detachAbortListener = () => signal.removeEventListener("abort", abort);
|
|
777
952
|
}
|
|
778
953
|
|
|
779
|
-
return
|
|
780
|
-
headers: {
|
|
781
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
782
|
-
"Cache-Control": "no-cache, no-transform",
|
|
783
|
-
Connection: "keep-alive",
|
|
784
|
-
},
|
|
785
|
-
});
|
|
954
|
+
return await sseHttpResponse(channel.stream, options);
|
|
786
955
|
}
|
|
787
956
|
|
|
788
957
|
function observeSseConnection(
|
|
@@ -843,20 +1012,179 @@ async function replayWorkspaceControlEvents(
|
|
|
843
1012
|
|
|
844
1013
|
class SseStreamStoppedError extends Error {}
|
|
845
1014
|
|
|
1015
|
+
async function reauthorizeSseOrClose(
|
|
1016
|
+
options: SseDeliveryOptions,
|
|
1017
|
+
channel: ByteBoundedSseStream,
|
|
1018
|
+
): Promise<void> {
|
|
1019
|
+
try {
|
|
1020
|
+
await options.reauthorize?.();
|
|
1021
|
+
} catch {
|
|
1022
|
+
// Authorization loss is an expected fail-closed terminal condition, not a
|
|
1023
|
+
// delivery fault. End the HTTP response cleanly so Bun and Chromium retire
|
|
1024
|
+
// the HTTP/1 socket even if the initiating document is being replaced.
|
|
1025
|
+
// No frame crosses the failed check; a still-live SDK may reconnect and is
|
|
1026
|
+
// fenced again at request admission, while a destroyed realm cannot leave
|
|
1027
|
+
// an errored response occupying the per-origin connection pool.
|
|
1028
|
+
channel.close();
|
|
1029
|
+
throw new SseStreamStoppedError();
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
846
1033
|
export type SseDeliveryOptions = {
|
|
1034
|
+
connectionLifetimeMs?: number | undefined;
|
|
1035
|
+
/** Return a known-length batch instead of a chunked response. HTTP/1 only. */
|
|
1036
|
+
finiteResponseMaxBytes?: number | undefined;
|
|
1037
|
+
/** Browser transport classification for a finite response. */
|
|
1038
|
+
finiteResponseMediaType?: "event-stream" | "http1-browser-batch" | undefined;
|
|
847
1039
|
maxQueuedBytes?: number;
|
|
848
1040
|
stallTimeoutMs?: number;
|
|
849
1041
|
heartbeatIntervalMs?: number;
|
|
850
1042
|
observability?: Observability | undefined;
|
|
851
1043
|
onObservation?: ((observation: SseDeliveryBoundObservation) => void) | undefined;
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
export type SessionSseDeliveryOptions = SseDeliveryOptions & {
|
|
855
|
-
/** Host ACL re-check, run even while the event stream is otherwise idle. */
|
|
1044
|
+
/** Current ACL re-check, run even while the event stream is idle. */
|
|
856
1045
|
reauthorize?: (() => Promise<void>) | undefined;
|
|
857
1046
|
reauthorizeAfterMs?: number | undefined;
|
|
1047
|
+
/** Exact selected actor emitted on the stream response for cross-tab fencing. */
|
|
1048
|
+
actorEpoch?: string | undefined;
|
|
858
1049
|
};
|
|
859
1050
|
|
|
1051
|
+
export function browserSseDeliveryOptions(
|
|
1052
|
+
transport: string | undefined,
|
|
1053
|
+
): Pick<
|
|
1054
|
+
SseDeliveryOptions,
|
|
1055
|
+
"connectionLifetimeMs" | "finiteResponseMaxBytes" | "finiteResponseMediaType"
|
|
1056
|
+
> {
|
|
1057
|
+
return transport === "http1-bounded"
|
|
1058
|
+
? {
|
|
1059
|
+
finiteResponseMaxBytes: HTTP1_BROWSER_SSE_BATCH_MAX_BYTES,
|
|
1060
|
+
finiteResponseMediaType: "http1-browser-batch",
|
|
1061
|
+
}
|
|
1062
|
+
: {};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async function sseHttpResponse(
|
|
1066
|
+
stream: ReadableStream<Uint8Array>,
|
|
1067
|
+
options: SseDeliveryOptions,
|
|
1068
|
+
): Promise<Response> {
|
|
1069
|
+
const maxBytes = options.finiteResponseMaxBytes;
|
|
1070
|
+
let body: ReadableStream<Uint8Array> | ArrayBuffer = stream;
|
|
1071
|
+
let contentLength: number | null = null;
|
|
1072
|
+
if (maxBytes !== undefined) {
|
|
1073
|
+
if (
|
|
1074
|
+
!Number.isSafeInteger(maxBytes) ||
|
|
1075
|
+
maxBytes < SESSION_EVENT_SSE_FRAME_MAX_BYTES ||
|
|
1076
|
+
maxBytes > HTTP1_BROWSER_SSE_BATCH_MAX_BYTES
|
|
1077
|
+
) {
|
|
1078
|
+
throw new RangeError(
|
|
1079
|
+
`finite SSE batch limit must be between ${SESSION_EVENT_SSE_FRAME_MAX_BYTES} and ${HTTP1_BROWSER_SSE_BATCH_MAX_BYTES} bytes`,
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
body = await collectFiniteSseBatch(stream, maxBytes);
|
|
1083
|
+
contentLength = body.byteLength;
|
|
1084
|
+
}
|
|
1085
|
+
return new Response(body, {
|
|
1086
|
+
headers: {
|
|
1087
|
+
// A bounded HTTP/1 poll carries the same SSE-framed bytes the SDK
|
|
1088
|
+
// already parses, but it is an ordinary finite response at the browser
|
|
1089
|
+
// transport boundary. Keeping `text/event-stream` here lets Chromium
|
|
1090
|
+
// retain an orphaned fetch in its shared per-origin SSE pool after the
|
|
1091
|
+
// initiating document is replaced, starving unrelated finite reads in
|
|
1092
|
+
// every tab. HTTP/2 and other unbounded streams keep the standard media
|
|
1093
|
+
// type; only the explicit `http1-bounded` fallback uses this vendor type.
|
|
1094
|
+
"Content-Type":
|
|
1095
|
+
contentLength !== null && options.finiteResponseMediaType === "http1-browser-batch"
|
|
1096
|
+
? `${HTTP1_BROWSER_SSE_BATCH_CONTENT_TYPE}; charset=utf-8`
|
|
1097
|
+
: "text/event-stream; charset=utf-8",
|
|
1098
|
+
"Cache-Control": "no-cache, no-transform",
|
|
1099
|
+
// A known-length response is already terminal and leaves the HTTP/1
|
|
1100
|
+
// socket reusable. Only a genuinely live SSE response needs an explicit
|
|
1101
|
+
// close when its stream ends.
|
|
1102
|
+
...(contentLength === null
|
|
1103
|
+
? { Connection: "close" }
|
|
1104
|
+
: { "Content-Length": String(contentLength) }),
|
|
1105
|
+
...(options.actorEpoch ? { [MANAGED_AUTH_ACTOR_EPOCH_HEADER]: options.actorEpoch } : {}),
|
|
1106
|
+
},
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function isHttp1BrowserBatch(options: SseDeliveryOptions): boolean {
|
|
1111
|
+
return (
|
|
1112
|
+
options.finiteResponseMediaType === "http1-browser-batch" &&
|
|
1113
|
+
options.finiteResponseMaxBytes !== undefined
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function finiteSseBatchResponse(frames: readonly string[], options: SseDeliveryOptions): Response {
|
|
1118
|
+
const maxBytes = options.finiteResponseMaxBytes;
|
|
1119
|
+
if (
|
|
1120
|
+
maxBytes === undefined ||
|
|
1121
|
+
!Number.isSafeInteger(maxBytes) ||
|
|
1122
|
+
maxBytes < SESSION_EVENT_SSE_FRAME_MAX_BYTES ||
|
|
1123
|
+
maxBytes > HTTP1_BROWSER_SSE_BATCH_MAX_BYTES
|
|
1124
|
+
) {
|
|
1125
|
+
throw new RangeError(
|
|
1126
|
+
`finite SSE batch limit must be between ${SESSION_EVENT_SSE_FRAME_MAX_BYTES} and ${HTTP1_BROWSER_SSE_BATCH_MAX_BYTES} bytes`,
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
const encoder = new TextEncoder();
|
|
1130
|
+
const chunks: Uint8Array[] = [];
|
|
1131
|
+
let length = 0;
|
|
1132
|
+
for (const frame of frames) {
|
|
1133
|
+
const chunk = encoder.encode(frame);
|
|
1134
|
+
if (chunks.length > 0 && length + chunk.byteLength > maxBytes) break;
|
|
1135
|
+
chunks.push(chunk);
|
|
1136
|
+
length += chunk.byteLength;
|
|
1137
|
+
}
|
|
1138
|
+
const body = new Uint8Array(length);
|
|
1139
|
+
let offset = 0;
|
|
1140
|
+
for (const chunk of chunks) {
|
|
1141
|
+
body.set(chunk, offset);
|
|
1142
|
+
offset += chunk.byteLength;
|
|
1143
|
+
}
|
|
1144
|
+
return new Response(body, {
|
|
1145
|
+
headers: {
|
|
1146
|
+
"Content-Type": `${HTTP1_BROWSER_SSE_BATCH_CONTENT_TYPE}; charset=utf-8`,
|
|
1147
|
+
"Cache-Control": "no-cache, no-transform",
|
|
1148
|
+
"Content-Length": String(body.byteLength),
|
|
1149
|
+
...(options.actorEpoch ? { [MANAGED_AUTH_ACTOR_EPOCH_HEADER]: options.actorEpoch } : {}),
|
|
1150
|
+
},
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
async function collectFiniteSseBatch(
|
|
1155
|
+
stream: ReadableStream<Uint8Array>,
|
|
1156
|
+
maxBytes: number,
|
|
1157
|
+
): Promise<ArrayBuffer> {
|
|
1158
|
+
const reader = stream.getReader();
|
|
1159
|
+
const chunks: Uint8Array[] = [];
|
|
1160
|
+
let length = 0;
|
|
1161
|
+
try {
|
|
1162
|
+
for (;;) {
|
|
1163
|
+
const next = await reader.read();
|
|
1164
|
+
if (next.done) break;
|
|
1165
|
+
// Each source chunk is one complete SSE frame. End before an overflowing
|
|
1166
|
+
// frame so reconnect replay starts from the consumer's last whole cursor.
|
|
1167
|
+
if (chunks.length > 0 && length + next.value.byteLength > maxBytes) {
|
|
1168
|
+
await reader.cancel("finite SSE batch reached its byte limit");
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
chunks.push(next.value);
|
|
1172
|
+
length += next.value.byteLength;
|
|
1173
|
+
}
|
|
1174
|
+
} finally {
|
|
1175
|
+
reader.releaseLock();
|
|
1176
|
+
}
|
|
1177
|
+
const bytes = new Uint8Array(length);
|
|
1178
|
+
let offset = 0;
|
|
1179
|
+
for (const chunk of chunks) {
|
|
1180
|
+
bytes.set(chunk, offset);
|
|
1181
|
+
offset += chunk.byteLength;
|
|
1182
|
+
}
|
|
1183
|
+
return bytes.buffer;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
export type SessionSseDeliveryOptions = SseDeliveryOptions;
|
|
1187
|
+
|
|
860
1188
|
function sseObservationReporter(
|
|
861
1189
|
stream: SseStreamKind,
|
|
862
1190
|
options: SseDeliveryOptions,
|
|
@@ -900,6 +1228,32 @@ function resolveInteractionPollInterval(value: number | undefined): number {
|
|
|
900
1228
|
return interval;
|
|
901
1229
|
}
|
|
902
1230
|
|
|
1231
|
+
function startSseReauthorization(
|
|
1232
|
+
options: SseDeliveryOptions,
|
|
1233
|
+
stopped: () => boolean,
|
|
1234
|
+
fail: (error: unknown) => void,
|
|
1235
|
+
): () => void {
|
|
1236
|
+
if (!options.reauthorize) return () => {};
|
|
1237
|
+
const interval = options.reauthorizeAfterMs ?? 15_000;
|
|
1238
|
+
if (!Number.isSafeInteger(interval) || interval < 1_000 || interval > 60_000) {
|
|
1239
|
+
throw new RangeError("SSE reauthorization must be between 1000 and 60000ms");
|
|
1240
|
+
}
|
|
1241
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
1242
|
+
const schedule = () => {
|
|
1243
|
+
if (stopped()) return;
|
|
1244
|
+
timer = setTimeout(() => {
|
|
1245
|
+
timer = null;
|
|
1246
|
+
void options.reauthorize!().then(schedule).catch(fail);
|
|
1247
|
+
}, interval);
|
|
1248
|
+
};
|
|
1249
|
+
schedule();
|
|
1250
|
+
return () => {
|
|
1251
|
+
if (!timer) return;
|
|
1252
|
+
clearTimeout(timer);
|
|
1253
|
+
timer = null;
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
|
|
903
1257
|
function formatWorkspaceInteractionRevisionSse(event: WorkspaceInteractionRevisionEvent): string {
|
|
904
1258
|
return `id: ${event.sequence}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
|
|
905
1259
|
}
|