@kortix/sdk 0.13.4 → 0.13.7
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/README.md +63 -6
- package/dist/browser/cache/idb-sync-cache.d.ts +19 -1
- package/dist/browser/cache/idb-sync-cache.d.ts.map +1 -1
- package/dist/browser/cache/idb-sync-cache.js +39 -4
- package/dist/browser/cache/idb-sync-cache.js.map +1 -1
- package/dist/browser/session-sync/server-transcript-mirror.d.ts +82 -0
- package/dist/browser/session-sync/server-transcript-mirror.d.ts.map +1 -0
- package/dist/browser/session-sync/server-transcript-mirror.js +143 -0
- package/dist/browser/session-sync/server-transcript-mirror.js.map +1 -0
- package/dist/browser/session-sync/session-sync-registry.d.ts +11 -1
- package/dist/browser/session-sync/session-sync-registry.d.ts.map +1 -1
- package/dist/browser/session-sync/session-sync-registry.js +117 -9
- package/dist/browser/session-sync/session-sync-registry.js.map +1 -1
- package/dist/browser/session-sync/visibility.d.ts +19 -0
- package/dist/browser/session-sync/visibility.d.ts.map +1 -0
- package/dist/browser/session-sync/visibility.js +16 -0
- package/dist/browser/session-sync/visibility.js.map +1 -0
- package/dist/browser/stores/sandbox-connection-store.d.ts +13 -1
- package/dist/browser/stores/sandbox-connection-store.d.ts.map +1 -1
- package/dist/browser/stores/sandbox-connection-store.js +6 -3
- package/dist/browser/stores/sandbox-connection-store.js.map +1 -1
- package/dist/browser/stores/session-working-store.d.ts +10 -5
- package/dist/browser/stores/session-working-store.d.ts.map +1 -1
- package/dist/browser/stores/session-working-store.js +32 -8
- package/dist/browser/stores/session-working-store.js.map +1 -1
- package/dist/browser/stores/sync-store.d.ts +56 -2
- package/dist/browser/stores/sync-store.d.ts.map +1 -1
- package/dist/browser/stores/sync-store.js +682 -402
- package/dist/browser/stores/sync-store.js.map +1 -1
- package/dist/core/auth/app-viewer.d.ts +57 -0
- package/dist/core/auth/app-viewer.d.ts.map +1 -0
- package/dist/core/auth/app-viewer.js +82 -0
- package/dist/core/auth/app-viewer.js.map +1 -0
- package/dist/core/auth/session.d.ts +45 -0
- package/dist/core/auth/session.d.ts.map +1 -0
- package/dist/core/auth/session.js +113 -0
- package/dist/core/auth/session.js.map +1 -0
- package/dist/core/client/kortix.d.ts +37 -29
- package/dist/core/client/kortix.d.ts.map +1 -1
- package/dist/core/client/kortix.js +61 -8
- package/dist/core/client/kortix.js.map +1 -1
- package/dist/core/http/config.d.ts +4 -2
- package/dist/core/http/config.d.ts.map +1 -1
- package/dist/core/http/config.js.map +1 -1
- package/dist/core/http/opencode-errors.d.ts +17 -0
- package/dist/core/http/opencode-errors.d.ts.map +1 -1
- package/dist/core/http/opencode-errors.js +25 -0
- package/dist/core/http/opencode-errors.js.map +1 -1
- package/dist/core/rest/platform-client/auth.d.ts +120 -0
- package/dist/core/rest/platform-client/auth.d.ts.map +1 -0
- package/dist/core/rest/platform-client/auth.js +98 -0
- package/dist/core/rest/platform-client/auth.js.map +1 -0
- package/dist/core/rest/platform-client/host-boundary.d.ts +5 -0
- package/dist/core/rest/platform-client/host-boundary.d.ts.map +1 -1
- package/dist/core/rest/platform-client/host-boundary.js.map +1 -1
- package/dist/core/rest/projects-client/accounts.d.ts +57 -0
- package/dist/core/rest/projects-client/accounts.d.ts.map +1 -1
- package/dist/core/rest/projects-client/accounts.js +31 -0
- package/dist/core/rest/projects-client/accounts.js.map +1 -1
- package/dist/core/rest/projects-client/apps.d.ts +9 -0
- package/dist/core/rest/projects-client/apps.d.ts.map +1 -1
- package/dist/core/rest/projects-client/apps.js.map +1 -1
- package/dist/core/rest/projects-client/billing.d.ts +2 -0
- package/dist/core/rest/projects-client/billing.d.ts.map +1 -1
- package/dist/core/rest/projects-client/billing.js.map +1 -1
- package/dist/core/rest/projects-client/channels.d.ts +0 -3
- package/dist/core/rest/projects-client/channels.d.ts.map +1 -1
- package/dist/core/rest/projects-client/channels.js +0 -6
- package/dist/core/rest/projects-client/channels.js.map +1 -1
- package/dist/core/rest/projects-client/connectors.d.ts +104 -19
- package/dist/core/rest/projects-client/connectors.d.ts.map +1 -1
- package/dist/core/rest/projects-client/connectors.js +56 -2
- package/dist/core/rest/projects-client/connectors.js.map +1 -1
- package/dist/core/rest/projects-client/index.d.ts +1 -2
- package/dist/core/rest/projects-client/index.d.ts.map +1 -1
- package/dist/core/rest/projects-client/index.js +1 -2
- package/dist/core/rest/projects-client/index.js.map +1 -1
- package/dist/core/rest/projects-client/oauth-clients.d.ts +47 -0
- package/dist/core/rest/projects-client/oauth-clients.d.ts.map +1 -0
- package/dist/core/rest/projects-client/oauth-clients.js +33 -0
- package/dist/core/rest/projects-client/oauth-clients.js.map +1 -0
- package/dist/core/rest/projects-client/projects.d.ts +23 -6
- package/dist/core/rest/projects-client/projects.d.ts.map +1 -1
- package/dist/core/rest/projects-client/projects.js +6 -3
- package/dist/core/rest/projects-client/projects.js.map +1 -1
- package/dist/core/rest/projects-client/session-sandbox.d.ts +52 -0
- package/dist/core/rest/projects-client/session-sandbox.d.ts.map +1 -1
- package/dist/core/rest/projects-client/session-sandbox.js.map +1 -1
- package/dist/core/rest/projects-client/sessions.d.ts +142 -34
- package/dist/core/rest/projects-client/sessions.d.ts.map +1 -1
- package/dist/core/rest/projects-client/sessions.js +48 -19
- package/dist/core/rest/projects-client/sessions.js.map +1 -1
- package/dist/core/runtime/attachment-part.d.ts +26 -0
- package/dist/core/runtime/attachment-part.d.ts.map +1 -0
- package/dist/core/runtime/attachment-part.js +41 -0
- package/dist/core/runtime/attachment-part.js.map +1 -0
- package/dist/core/runtime/client.d.ts +4 -2
- package/dist/core/runtime/client.d.ts.map +1 -1
- package/dist/core/runtime/client.js +3 -2
- package/dist/core/runtime/client.js.map +1 -1
- package/dist/core/runtime/opencode-v2-event-aliases.d.ts +184 -0
- package/dist/core/runtime/opencode-v2-event-aliases.d.ts.map +1 -0
- package/dist/core/runtime/opencode-v2-event-aliases.js +2 -0
- package/dist/core/runtime/opencode-v2-event-aliases.js.map +1 -0
- package/dist/core/runtime/pty.d.ts +11 -1
- package/dist/core/runtime/pty.d.ts.map +1 -1
- package/dist/core/runtime/pty.js +19 -6
- package/dist/core/runtime/pty.js.map +1 -1
- package/dist/core/session/connection.d.ts +64 -0
- package/dist/core/session/connection.d.ts.map +1 -0
- package/dist/core/session/connection.js +36 -0
- package/dist/core/session/connection.js.map +1 -0
- package/dist/core/session/index.d.ts +2 -0
- package/dist/core/session/index.d.ts.map +1 -1
- package/dist/core/session/index.js +2 -0
- package/dist/core/session/index.js.map +1 -1
- package/dist/core/session/open-bundle.d.ts +110 -0
- package/dist/core/session/open-bundle.d.ts.map +1 -0
- package/dist/core/session/open-bundle.js +183 -0
- package/dist/core/session/open-bundle.js.map +1 -0
- package/dist/core/session/poll-owner.d.ts +35 -0
- package/dist/core/session/poll-owner.d.ts.map +1 -0
- package/dist/core/session/poll-owner.js +90 -0
- package/dist/core/session/poll-owner.js.map +1 -0
- package/dist/core/session/preview-auth.d.ts +25 -0
- package/dist/core/session/preview-auth.d.ts.map +1 -1
- package/dist/core/session/preview-auth.js +31 -1
- package/dist/core/session/preview-auth.js.map +1 -1
- package/dist/core/session/preview-config.d.ts +26 -0
- package/dist/core/session/preview-config.d.ts.map +1 -0
- package/dist/core/session/preview-config.js +95 -0
- package/dist/core/session/preview-config.js.map +1 -0
- package/dist/core/session/preview-options.d.ts +25 -0
- package/dist/core/session/preview-options.d.ts.map +1 -0
- package/dist/core/session/preview-options.js +45 -0
- package/dist/core/session/preview-options.js.map +1 -0
- package/dist/core/session/preview.d.ts +8 -3
- package/dist/core/session/preview.d.ts.map +1 -1
- package/dist/core/session/preview.js +11 -4
- package/dist/core/session/preview.js.map +1 -1
- package/dist/core/session/rewind.d.ts +67 -24
- package/dist/core/session/rewind.d.ts.map +1 -1
- package/dist/core/session/rewind.js +74 -26
- package/dist/core/session/rewind.js.map +1 -1
- package/dist/core/session/server-store/active.d.ts +10 -12
- package/dist/core/session/server-store/active.d.ts.map +1 -1
- package/dist/core/session/server-store/active.js +11 -9
- package/dist/core/session/server-store/active.js.map +1 -1
- package/dist/core/session/server-store/url-helpers.d.ts +11 -2
- package/dist/core/session/server-store/url-helpers.d.ts.map +1 -1
- package/dist/core/session/server-store/url-helpers.js +11 -2
- package/dist/core/session/server-store/url-helpers.js.map +1 -1
- package/dist/core/session/static-file-preview.d.ts +50 -0
- package/dist/core/session/static-file-preview.d.ts.map +1 -0
- package/dist/core/session/static-file-preview.js +91 -0
- package/dist/core/session/static-file-preview.js.map +1 -0
- package/dist/core/session/url.d.ts +25 -2
- package/dist/core/session/url.d.ts.map +1 -1
- package/dist/core/session/url.js +87 -11
- package/dist/core/session/url.js.map +1 -1
- package/dist/core/session/wake-escalation.d.ts +188 -0
- package/dist/core/session/wake-escalation.d.ts.map +1 -0
- package/dist/core/session/wake-escalation.js +241 -0
- package/dist/core/session/wake-escalation.js.map +1 -0
- package/dist/core/session/working.d.ts +107 -2
- package/dist/core/session/working.d.ts.map +1 -1
- package/dist/core/session/working.js +206 -7
- package/dist/core/session/working.js.map +1 -1
- package/dist/core/session-sync/durable-log.d.ts +96 -0
- package/dist/core/session-sync/durable-log.d.ts.map +1 -0
- package/dist/core/session-sync/durable-log.js +94 -0
- package/dist/core/session-sync/durable-log.js.map +1 -0
- package/dist/core/session-sync/fragment.d.ts +27 -0
- package/dist/core/session-sync/fragment.d.ts.map +1 -0
- package/dist/core/session-sync/fragment.js +24 -0
- package/dist/core/session-sync/fragment.js.map +1 -0
- package/dist/core/session-sync/session-sync-controller.d.ts +117 -11
- package/dist/core/session-sync/session-sync-controller.d.ts.map +1 -1
- package/dist/core/session-sync/session-sync-controller.js +278 -26
- package/dist/core/session-sync/session-sync-controller.js.map +1 -1
- package/dist/core/stream/keepalive.d.ts +17 -0
- package/dist/core/stream/keepalive.d.ts.map +1 -0
- package/dist/core/stream/keepalive.js +19 -0
- package/dist/core/stream/keepalive.js.map +1 -0
- package/dist/core/turns/grouping.d.ts.map +1 -1
- package/dist/core/turns/grouping.js +73 -23
- package/dist/core/turns/grouping.js.map +1 -1
- package/dist/core/turns/types.d.ts +5 -0
- package/dist/core/turns/types.d.ts.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -1
- package/dist/kortix.esm.min.js +25 -25
- package/dist/kortix.global.js +25 -25
- package/dist/node/app-viewer.d.ts +41 -0
- package/dist/node/app-viewer.d.ts.map +1 -0
- package/dist/node/app-viewer.js +150 -0
- package/dist/node/app-viewer.js.map +1 -0
- package/dist/node/auth.d.ts +84 -0
- package/dist/node/auth.d.ts.map +1 -0
- package/dist/node/auth.js +414 -0
- package/dist/node/auth.js.map +1 -0
- package/dist/node/server.d.ts +19 -0
- package/dist/node/server.d.ts.map +1 -1
- package/dist/node/server.js +13 -1
- package/dist/node/server.js.map +1 -1
- package/dist/platform/auth-core.d.ts +13 -1
- package/dist/platform/auth-core.d.ts.map +1 -1
- package/dist/platform/auth-core.js +13 -1
- package/dist/platform/auth-core.js.map +1 -1
- package/dist/react/bedrock-invokable.d.ts +58 -0
- package/dist/react/bedrock-invokable.d.ts.map +1 -0
- package/dist/react/bedrock-invokable.js +83 -0
- package/dist/react/bedrock-invokable.js.map +1 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +7 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/model-flatten.d.ts +14 -1
- package/dist/react/model-flatten.d.ts.map +1 -1
- package/dist/react/model-flatten.js +3 -4
- package/dist/react/model-flatten.js.map +1 -1
- package/dist/react/opencode.d.ts +3 -1
- package/dist/react/opencode.d.ts.map +1 -1
- package/dist/react/opencode.js +5 -1
- package/dist/react/opencode.js.map +1 -1
- package/dist/react/provider-selection.d.ts +58 -0
- package/dist/react/provider-selection.d.ts.map +1 -1
- package/dist/react/provider-selection.js +253 -2
- package/dist/react/provider-selection.js.map +1 -1
- package/dist/react/session-start-giveup.d.ts +12 -0
- package/dist/react/session-start-giveup.d.ts.map +1 -0
- package/dist/react/session-start-giveup.js +13 -0
- package/dist/react/session-start-giveup.js.map +1 -0
- package/dist/react/session-title-sync.d.ts.map +1 -1
- package/dist/react/session-title-sync.js +15 -5
- package/dist/react/session-title-sync.js.map +1 -1
- package/dist/react/sign-in-with-kortix.d.ts +346 -0
- package/dist/react/sign-in-with-kortix.d.ts.map +1 -0
- package/dist/react/sign-in-with-kortix.js +65 -0
- package/dist/react/sign-in-with-kortix.js.map +1 -0
- package/dist/react/use-admin-accounts.d.ts +36 -1
- package/dist/react/use-admin-accounts.d.ts.map +1 -1
- package/dist/react/use-admin-accounts.js +19 -0
- package/dist/react/use-admin-accounts.js.map +1 -1
- package/dist/react/use-gateway-routing-policy.d.ts +1 -0
- package/dist/react/use-gateway-routing-policy.d.ts.map +1 -1
- package/dist/react/use-gateway-routing-policy.js +12 -1
- package/dist/react/use-gateway-routing-policy.js.map +1 -1
- package/dist/react/use-kortix-app-viewer.d.ts +13 -0
- package/dist/react/use-kortix-app-viewer.d.ts.map +1 -0
- package/dist/react/use-kortix-app-viewer.js +29 -0
- package/dist/react/use-kortix-app-viewer.js.map +1 -0
- package/dist/react/use-model-defaults.d.ts +7 -0
- package/dist/react/use-model-defaults.d.ts.map +1 -1
- package/dist/react/use-model-defaults.js +7 -1
- package/dist/react/use-model-defaults.js.map +1 -1
- package/dist/react/use-model-enablement.d.ts.map +1 -1
- package/dist/react/use-model-enablement.js +5 -1
- package/dist/react/use-model-enablement.js.map +1 -1
- package/dist/react/use-model-store.d.ts +31 -0
- package/dist/react/use-model-store.d.ts.map +1 -1
- package/dist/react/use-model-store.js +31 -2
- package/dist/react/use-model-store.js.map +1 -1
- package/dist/react/use-opencode-events/handle-event.d.ts.map +1 -1
- package/dist/react/use-opencode-events/handle-event.js +26 -0
- package/dist/react/use-opencode-events/handle-event.js.map +1 -1
- package/dist/react/use-opencode-events/helpers.d.ts +49 -5
- package/dist/react/use-opencode-events/helpers.d.ts.map +1 -1
- package/dist/react/use-opencode-events/helpers.js +54 -6
- package/dist/react/use-opencode-events/helpers.js.map +1 -1
- package/dist/react/use-opencode-events/index.d.ts.map +1 -1
- package/dist/react/use-opencode-events/index.js +80 -8
- package/dist/react/use-opencode-events/index.js.map +1 -1
- package/dist/react/use-opencode-events/rehydrate-targets.d.ts +18 -0
- package/dist/react/use-opencode-events/rehydrate-targets.d.ts.map +1 -0
- package/dist/react/use-opencode-events/rehydrate-targets.js +20 -0
- package/dist/react/use-opencode-events/rehydrate-targets.js.map +1 -0
- package/dist/react/use-opencode-events/stream-revival.d.ts +37 -0
- package/dist/react/use-opencode-events/stream-revival.d.ts.map +1 -0
- package/dist/react/use-opencode-events/stream-revival.js +53 -0
- package/dist/react/use-opencode-events/stream-revival.js.map +1 -0
- package/dist/react/use-opencode-events/use-event-stream-refs.d.ts.map +1 -1
- package/dist/react/use-opencode-events/use-event-stream-refs.js +14 -2
- package/dist/react/use-opencode-events/use-event-stream-refs.js.map +1 -1
- package/dist/react/use-opencode-local.d.ts.map +1 -1
- package/dist/react/use-opencode-local.js +27 -8
- package/dist/react/use-opencode-local.js.map +1 -1
- package/dist/react/use-opencode-pty.d.ts +7 -1
- package/dist/react/use-opencode-pty.d.ts.map +1 -1
- package/dist/react/use-opencode-pty.js +6 -2
- package/dist/react/use-opencode-pty.js.map +1 -1
- package/dist/react/use-opencode-sessions/agents.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/agents.js +13 -2
- package/dist/react/use-opencode-sessions/agents.js.map +1 -1
- package/dist/react/use-opencode-sessions/commands.d.ts +12 -0
- package/dist/react/use-opencode-sessions/commands.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/commands.js +15 -3
- package/dist/react/use-opencode-sessions/commands.js.map +1 -1
- package/dist/react/use-opencode-sessions/index.d.ts +1 -0
- package/dist/react/use-opencode-sessions/index.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/index.js +1 -0
- package/dist/react/use-opencode-sessions/index.js.map +1 -1
- package/dist/react/use-opencode-sessions/keys.d.ts +8 -0
- package/dist/react/use-opencode-sessions/keys.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/keys.js +8 -0
- package/dist/react/use-opencode-sessions/keys.js.map +1 -1
- package/dist/react/use-opencode-sessions/providers.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/providers.js +56 -5
- package/dist/react/use-opencode-sessions/providers.js.map +1 -1
- package/dist/react/use-opencode-sessions/shared.d.ts +23 -0
- package/dist/react/use-opencode-sessions/shared.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/shared.js +31 -0
- package/dist/react/use-opencode-sessions/shared.js.map +1 -1
- package/dist/react/use-opencode-sessions/vcs.d.ts +28 -0
- package/dist/react/use-opencode-sessions/vcs.d.ts.map +1 -0
- package/dist/react/use-opencode-sessions/vcs.js +42 -0
- package/dist/react/use-opencode-sessions/vcs.js.map +1 -0
- package/dist/react/use-poll-owner.d.ts +14 -0
- package/dist/react/use-poll-owner.d.ts.map +1 -0
- package/dist/react/use-poll-owner.js +32 -0
- package/dist/react/use-poll-owner.js.map +1 -0
- package/dist/react/use-project-llm-gateway.d.ts +27 -0
- package/dist/react/use-project-llm-gateway.d.ts.map +1 -0
- package/dist/react/use-project-llm-gateway.js +41 -0
- package/dist/react/use-project-llm-gateway.js.map +1 -0
- package/dist/react/use-project-models.d.ts +9 -0
- package/dist/react/use-project-models.d.ts.map +1 -1
- package/dist/react/use-project-models.js +24 -1
- package/dist/react/use-project-models.js.map +1 -1
- package/dist/react/use-runtime-reconnect.d.ts +11 -0
- package/dist/react/use-runtime-reconnect.d.ts.map +1 -1
- package/dist/react/use-runtime-reconnect.js +34 -3
- package/dist/react/use-runtime-reconnect.js.map +1 -1
- package/dist/react/use-session-prompts.d.ts +53 -3
- package/dist/react/use-session-prompts.d.ts.map +1 -1
- package/dist/react/use-session-prompts.js +133 -21
- package/dist/react/use-session-prompts.js.map +1 -1
- package/dist/react/use-session-send.d.ts +17 -0
- package/dist/react/use-session-send.d.ts.map +1 -1
- package/dist/react/use-session-send.js +54 -5
- package/dist/react/use-session-send.js.map +1 -1
- package/dist/react/use-session-start-give-up.d.ts +20 -0
- package/dist/react/use-session-start-give-up.d.ts.map +1 -0
- package/dist/react/use-session-start-give-up.js +60 -0
- package/dist/react/use-session-start-give-up.js.map +1 -0
- package/dist/react/use-session-sync.d.ts +35 -1
- package/dist/react/use-session-sync.d.ts.map +1 -1
- package/dist/react/use-session-sync.js +135 -53
- package/dist/react/use-session-sync.js.map +1 -1
- package/dist/react/use-session-working.d.ts +55 -0
- package/dist/react/use-session-working.d.ts.map +1 -1
- package/dist/react/use-session-working.js +114 -13
- package/dist/react/use-session-working.js.map +1 -1
- package/dist/react/use-session.d.ts +13 -13
- package/dist/react/use-session.d.ts.map +1 -1
- package/dist/react/use-session.js +74 -53
- package/dist/react/use-session.js.map +1 -1
- package/dist/react/use-static-file-preview.d.ts +12 -0
- package/dist/react/use-static-file-preview.d.ts.map +1 -0
- package/dist/react/use-static-file-preview.js +119 -0
- package/dist/react/use-static-file-preview.js.map +1 -0
- package/dist/react/use-wake-escalation.d.ts +51 -0
- package/dist/react/use-wake-escalation.d.ts.map +1 -0
- package/dist/react/use-wake-escalation.js +62 -0
- package/dist/react/use-wake-escalation.js.map +1 -0
- package/package.json +6 -4
- package/dist/browser/session-sync/session-transcript-cache.d.ts +0 -79
- package/dist/browser/session-sync/session-transcript-cache.d.ts.map +0 -1
- package/dist/browser/session-sync/session-transcript-cache.js +0 -91
- package/dist/browser/session-sync/session-transcript-cache.js.map +0 -1
- package/dist/core/rest/projects-client/public-voice-join.d.ts +0 -59
- package/dist/core/rest/projects-client/public-voice-join.d.ts.map +0 -1
- package/dist/core/rest/projects-client/public-voice-join.js +0 -64
- package/dist/core/rest/projects-client/public-voice-join.js.map +0 -1
- package/dist/core/rest/projects-client/transcription.d.ts +0 -5
- package/dist/core/rest/projects-client/transcription.d.ts.map +0 -1
- package/dist/core/rest/projects-client/transcription.js +0 -14
- package/dist/core/rest/projects-client/transcription.js.map +0 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE session-open read, shared by every consumer that would otherwise issue
|
|
3
|
+
* its own.
|
|
4
|
+
*
|
|
5
|
+
* THE PROBLEM. Opening a session made 6 serial control-plane round trips before
|
|
6
|
+
* the first honest frame — the session row, `/turn`, `/prompts`,
|
|
7
|
+
* `/transcript?shape=sync`, `/detail`, `/model-defaults` — and then kept making
|
|
8
|
+
* them, because the hooks that need those facts each own their own query. On a
|
|
9
|
+
* real deployment that is 0.3-2.3 s per read at the median (measured over 20
|
|
10
|
+
* session opens, 2026-08-26), and the last one cannot start until the first
|
|
11
|
+
* lands.
|
|
12
|
+
*
|
|
13
|
+
* THE SHAPE. `GET .../open-bundle` answers all of it at once, and this module
|
|
14
|
+
* is the seam that lets the EXISTING consumers use it without knowing it exists:
|
|
15
|
+
*
|
|
16
|
+
* 1. The open path calls `openSessionBundle()` ONCE, in a LAYOUT effect. React
|
|
17
|
+
* runs every layout effect, tree-wide, before any passive effect, and
|
|
18
|
+
* TanStack starts a query's fetch from a passive effect — so the bundle is
|
|
19
|
+
* always in flight before the first `/turn` or `/prompts` fetch begins.
|
|
20
|
+
* 2. Each consumer's own `queryFn` calls `claimOpenBundle()` FIRST. A claim
|
|
21
|
+
* returns the shared promise while the bundle is in flight, or the resolved
|
|
22
|
+
* bundle inside a short share window, and `null` otherwise.
|
|
23
|
+
* 3. `null` means "answer the way you always did". Every consumer keeps its
|
|
24
|
+
* own endpoint, so an old server, a failed bundle and a steady-state poll
|
|
25
|
+
* all still work. The bundle is an ACCELERATOR, never a dependency.
|
|
26
|
+
*
|
|
27
|
+
* WHY A CLAIM CANNOT START ONE. A `/turn` poll that could start a bundle would
|
|
28
|
+
* re-read the transcript and the model catalogue every 15 s for the life of the
|
|
29
|
+
* tab. Starting is the open path's decision; claiming is everyone else's.
|
|
30
|
+
*
|
|
31
|
+
* WHY THE TRANSCRIPT IS A SEPARATE, ONE-SHOT STASH. The mirror is hydrated once
|
|
32
|
+
* the session's OpenCode root is known, which on a cold box is 18.9-24.5 s after
|
|
33
|
+
* the open (measured) — long past the share window the turn and queue legs want.
|
|
34
|
+
* It is also the one leg that must never be served twice: a second hydrate over
|
|
35
|
+
* a store the runtime has already filled is how a transcript grows ghosts.
|
|
36
|
+
*
|
|
37
|
+
* EVERY LEG IS TRI-STATE. `known: false` projects to `null`, which sends the
|
|
38
|
+
* consumer to its endpoint. It must never project to the NEGATIVE answer — an
|
|
39
|
+
* empty queue, an idle turn, an empty thread — because a default rendered as an
|
|
40
|
+
* answer is the defect the bundle exists to remove.
|
|
41
|
+
*/
|
|
42
|
+
import { getSessionOpenBundle, } from '../rest/projects-client/sessions.js';
|
|
43
|
+
/**
|
|
44
|
+
* How long a resolved bundle keeps answering claims.
|
|
45
|
+
*
|
|
46
|
+
* It only has to outlast ONE session open's burst of first reads, not the
|
|
47
|
+
* session. Long enough and the steady-state `/turn` poll (5 s working / 15 s
|
|
48
|
+
* idle) would be answered from a snapshot instead of from the server; this
|
|
49
|
+
* window sits just under the fastest of those cadences on purpose.
|
|
50
|
+
*/
|
|
51
|
+
export const OPEN_BUNDLE_SHARE_MS = 5_000;
|
|
52
|
+
/** How long the one-shot transcript stash survives. Sized for the slowest
|
|
53
|
+
* measured cold box (24.5 s Platinum) with headroom, because the hydrate that
|
|
54
|
+
* consumes it cannot run until the runtime identity resolves. */
|
|
55
|
+
export const OPEN_BUNDLE_TRANSCRIPT_TTL_MS = 60_000;
|
|
56
|
+
/** The mirrored-message window an open asks for — the same span the sync
|
|
57
|
+
* controller's own first read covers. */
|
|
58
|
+
export const OPEN_BUNDLE_TRANSCRIPT_LIMIT = 40;
|
|
59
|
+
const entries = new Map();
|
|
60
|
+
const transcripts = new Map();
|
|
61
|
+
function scopeKey(projectId, sessionId) {
|
|
62
|
+
return `${projectId}/${sessionId}`;
|
|
63
|
+
}
|
|
64
|
+
/** Drop everything. Tests only — a module singleton with no reset is a test
|
|
65
|
+
* that passes because of the one before it. */
|
|
66
|
+
export function resetSessionOpenBundles() {
|
|
67
|
+
entries.clear();
|
|
68
|
+
transcripts.clear();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Start the session-open read for `(projectId, sessionId)`.
|
|
72
|
+
*
|
|
73
|
+
* Fire-and-forget and idempotent within the share window: calling it twice for
|
|
74
|
+
* one open (two hooks, StrictMode's double mount) issues one request. Never
|
|
75
|
+
* throws and never rejects — a failed bundle simply resolves `null` and every
|
|
76
|
+
* consumer falls back to its own endpoint.
|
|
77
|
+
*/
|
|
78
|
+
export function openSessionBundle(projectId, sessionId, options = {}) {
|
|
79
|
+
if (!projectId || !sessionId)
|
|
80
|
+
return;
|
|
81
|
+
const now = options.now ?? Date.now;
|
|
82
|
+
const nowMs = now();
|
|
83
|
+
const key = scopeKey(projectId, sessionId);
|
|
84
|
+
const existing = entries.get(key);
|
|
85
|
+
if (existing && claimable(existing, nowMs))
|
|
86
|
+
return;
|
|
87
|
+
const entry = {
|
|
88
|
+
startedAtMs: nowMs,
|
|
89
|
+
settledAtMs: null,
|
|
90
|
+
promise: getSessionOpenBundle(projectId, sessionId, {
|
|
91
|
+
transcript: options.transcript ?? OPEN_BUNDLE_TRANSCRIPT_LIMIT,
|
|
92
|
+
})
|
|
93
|
+
.then((bundle) => {
|
|
94
|
+
stashTranscript(key, bundle, now());
|
|
95
|
+
return bundle;
|
|
96
|
+
})
|
|
97
|
+
.catch(() => null),
|
|
98
|
+
};
|
|
99
|
+
entry.promise = entry.promise.then((bundle) => {
|
|
100
|
+
entry.settledAtMs = now();
|
|
101
|
+
return bundle;
|
|
102
|
+
});
|
|
103
|
+
entries.set(key, entry);
|
|
104
|
+
}
|
|
105
|
+
function claimable(entry, nowMs) {
|
|
106
|
+
// In flight is always claimable: the consumer that asks is precisely the one
|
|
107
|
+
// this read was started for.
|
|
108
|
+
if (entry.settledAtMs === null)
|
|
109
|
+
return true;
|
|
110
|
+
return nowMs - entry.settledAtMs <= OPEN_BUNDLE_SHARE_MS;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Claim the in-flight or freshly-resolved bundle for this session.
|
|
114
|
+
*
|
|
115
|
+
* Returns the shared promise, or `null` when there is nothing to share — which
|
|
116
|
+
* is the signal to read the endpoint the normal way. Synchronous in the `null`
|
|
117
|
+
* case on purpose: a consumer must be able to decide without awaiting.
|
|
118
|
+
*/
|
|
119
|
+
export function claimOpenBundle(projectId, sessionId, nowMs = Date.now()) {
|
|
120
|
+
if (!projectId || !sessionId)
|
|
121
|
+
return null;
|
|
122
|
+
const key = scopeKey(projectId, sessionId);
|
|
123
|
+
const entry = entries.get(key);
|
|
124
|
+
if (!entry)
|
|
125
|
+
return null;
|
|
126
|
+
if (!claimable(entry, nowMs)) {
|
|
127
|
+
entries.delete(key);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return entry.promise;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Project the turn leg. `null` for an unknown leg — never `{ turns: [] }`,
|
|
134
|
+
* which would be an idle claim made by something that did not know.
|
|
135
|
+
*/
|
|
136
|
+
export function openBundleTurn(bundle) {
|
|
137
|
+
const turn = bundle.turn;
|
|
138
|
+
if (!turn || turn.known !== true)
|
|
139
|
+
return null;
|
|
140
|
+
const observedAtMs = Date.parse(bundle.observed_at);
|
|
141
|
+
return {
|
|
142
|
+
turns: turn.turns ?? [],
|
|
143
|
+
last_ended: turn.last_ended,
|
|
144
|
+
atMs: Number.isFinite(observedAtMs) ? observedAtMs : Date.now(),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** Project the queue leg. `null` for an unknown leg — never `[]`, which would
|
|
148
|
+
* render as "nothing queued" for a queue nobody could read. */
|
|
149
|
+
export function openBundleQueue(bundle) {
|
|
150
|
+
const queue = bundle.queue;
|
|
151
|
+
if (!queue || queue.known !== true)
|
|
152
|
+
return null;
|
|
153
|
+
return queue.prompts ?? [];
|
|
154
|
+
}
|
|
155
|
+
function stashTranscript(key, bundle, nowMs) {
|
|
156
|
+
const transcript = bundle.transcript;
|
|
157
|
+
if (!transcript || transcript.known !== true || transcript.requested !== true)
|
|
158
|
+
return;
|
|
159
|
+
// An unavailable mirror is not a transcript. Stashing it would let a hydrate
|
|
160
|
+
// paint an empty thread as a complete one.
|
|
161
|
+
if (!transcript.available || transcript.messages.length === 0)
|
|
162
|
+
return;
|
|
163
|
+
const { known: _known, requested: _requested, ...envelope } = transcript;
|
|
164
|
+
transcripts.set(key, { envelope, stashedAtMs: nowMs });
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Take the mirrored transcript this open fetched, ONCE.
|
|
168
|
+
*
|
|
169
|
+
* One-shot by design: the hydrate that consumes it is the only reader that may
|
|
170
|
+
* paint a snapshot, and a second paint over a store the runtime has already
|
|
171
|
+
* filled is how a transcript grows ghosts.
|
|
172
|
+
*/
|
|
173
|
+
export function takeOpenBundleTranscript(projectId, sessionId, nowMs = Date.now()) {
|
|
174
|
+
const key = scopeKey(projectId, sessionId);
|
|
175
|
+
const stash = transcripts.get(key);
|
|
176
|
+
if (!stash)
|
|
177
|
+
return null;
|
|
178
|
+
transcripts.delete(key);
|
|
179
|
+
if (nowMs - stash.stashedAtMs > OPEN_BUNDLE_TRANSCRIPT_TTL_MS)
|
|
180
|
+
return null;
|
|
181
|
+
return stash.envelope;
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=open-bundle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open-bundle.js","sourceRoot":"","sources":["../../../src/core/session/open-bundle.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE cadence per session fact, however many components read it.
|
|
3
|
+
*
|
|
4
|
+
* THE PROBLEM. TanStack schedules `refetchInterval` PER OBSERVER, not per
|
|
5
|
+
* query. Three hooks mount the `/turn` query on a session route (`useSession`,
|
|
6
|
+
* the composer, the session panel) and two mount `/prompts`, so one key ran
|
|
7
|
+
* three timers and the session was polled at three times its declared cadence.
|
|
8
|
+
* Measured on a real deployment: 6 `GET .../turn` inside a single 25 s session
|
|
9
|
+
* open, against a 5 s working / 15 s idle contract.
|
|
10
|
+
*
|
|
11
|
+
* THE RULE. Exactly one observer per scope owns the interval; the rest pass
|
|
12
|
+
* `refetchInterval: false` and read the same cache entry the owner refreshes.
|
|
13
|
+
* They share a query key, so an answer reaches every observer either way —
|
|
14
|
+
* only the SCHEDULING is exclusive.
|
|
15
|
+
*
|
|
16
|
+
* OWNERSHIP MUST SURVIVE THE OWNER. A session whose only poller unmounted would
|
|
17
|
+
* stop learning that a turn started somewhere else (a trigger, a second device,
|
|
18
|
+
* the inbox delivering a queued prompt), so releasing hands the cadence to the
|
|
19
|
+
* next claimant and notifies it.
|
|
20
|
+
*
|
|
21
|
+
* Framework-free on purpose: `react/use-poll-owner.ts` is a thin
|
|
22
|
+
* `useSyncExternalStore` over this, and the rules live where they can be tested
|
|
23
|
+
* without rendering anything.
|
|
24
|
+
*/
|
|
25
|
+
/** Register `id` as a candidate poller for `scope`. Idempotent. */
|
|
26
|
+
export declare function claimPoller(scope: string, id: string): void;
|
|
27
|
+
/** Give up `id`'s claim. If it held the cadence, the next claimant takes it. */
|
|
28
|
+
export declare function releasePoller(scope: string, id: string): void;
|
|
29
|
+
/** Does `id` own `scope`'s cadence right now? */
|
|
30
|
+
export declare function isPollOwner(scope: string, id: string): boolean;
|
|
31
|
+
/** Re-render me when this scope's owner changes. Returns the unsubscribe. */
|
|
32
|
+
export declare function subscribePollOwner(scope: string, listener: () => void): () => void;
|
|
33
|
+
/** Drop everything. Tests only. */
|
|
34
|
+
export declare function resetPollOwners(): void;
|
|
35
|
+
//# sourceMappingURL=poll-owner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poll-owner.d.ts","sourceRoot":"","sources":["../../../src/core/session/poll-owner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AA8BH,mEAAmE;AACnE,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAO3D;AAED,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAS7D;AAED,iDAAiD;AACjD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAI9D;AAED,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAOlF;AAED,mCAAmC;AACnC,wBAAgB,eAAe,IAAI,IAAI,CAEtC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE cadence per session fact, however many components read it.
|
|
3
|
+
*
|
|
4
|
+
* THE PROBLEM. TanStack schedules `refetchInterval` PER OBSERVER, not per
|
|
5
|
+
* query. Three hooks mount the `/turn` query on a session route (`useSession`,
|
|
6
|
+
* the composer, the session panel) and two mount `/prompts`, so one key ran
|
|
7
|
+
* three timers and the session was polled at three times its declared cadence.
|
|
8
|
+
* Measured on a real deployment: 6 `GET .../turn` inside a single 25 s session
|
|
9
|
+
* open, against a 5 s working / 15 s idle contract.
|
|
10
|
+
*
|
|
11
|
+
* THE RULE. Exactly one observer per scope owns the interval; the rest pass
|
|
12
|
+
* `refetchInterval: false` and read the same cache entry the owner refreshes.
|
|
13
|
+
* They share a query key, so an answer reaches every observer either way —
|
|
14
|
+
* only the SCHEDULING is exclusive.
|
|
15
|
+
*
|
|
16
|
+
* OWNERSHIP MUST SURVIVE THE OWNER. A session whose only poller unmounted would
|
|
17
|
+
* stop learning that a turn started somewhere else (a trigger, a second device,
|
|
18
|
+
* the inbox delivering a queued prompt), so releasing hands the cadence to the
|
|
19
|
+
* next claimant and notifies it.
|
|
20
|
+
*
|
|
21
|
+
* Framework-free on purpose: `react/use-poll-owner.ts` is a thin
|
|
22
|
+
* `useSyncExternalStore` over this, and the rules live where they can be tested
|
|
23
|
+
* without rendering anything.
|
|
24
|
+
*/
|
|
25
|
+
const scopes = new Map();
|
|
26
|
+
function stateFor(scope) {
|
|
27
|
+
let state = scopes.get(scope);
|
|
28
|
+
if (!state) {
|
|
29
|
+
state = { claimants: [], listeners: new Set() };
|
|
30
|
+
scopes.set(scope, state);
|
|
31
|
+
}
|
|
32
|
+
return state;
|
|
33
|
+
}
|
|
34
|
+
function notify(state) {
|
|
35
|
+
for (const listener of state.listeners)
|
|
36
|
+
listener();
|
|
37
|
+
}
|
|
38
|
+
function forget(scope, state) {
|
|
39
|
+
// Leak hygiene: a per-session registry that never shrinks is a per-session
|
|
40
|
+
// leak. A scope with no claimants and no listeners has nothing to remember.
|
|
41
|
+
if (state.claimants.length === 0 && state.listeners.size === 0)
|
|
42
|
+
scopes.delete(scope);
|
|
43
|
+
}
|
|
44
|
+
/** Register `id` as a candidate poller for `scope`. Idempotent. */
|
|
45
|
+
export function claimPoller(scope, id) {
|
|
46
|
+
if (!scope || !id)
|
|
47
|
+
return;
|
|
48
|
+
const state = stateFor(scope);
|
|
49
|
+
if (state.claimants.includes(id))
|
|
50
|
+
return;
|
|
51
|
+
const wasOwner = state.claimants[0];
|
|
52
|
+
state.claimants.push(id);
|
|
53
|
+
if (state.claimants[0] !== wasOwner)
|
|
54
|
+
notify(state);
|
|
55
|
+
}
|
|
56
|
+
/** Give up `id`'s claim. If it held the cadence, the next claimant takes it. */
|
|
57
|
+
export function releasePoller(scope, id) {
|
|
58
|
+
const state = scopes.get(scope);
|
|
59
|
+
if (!state)
|
|
60
|
+
return;
|
|
61
|
+
const index = state.claimants.indexOf(id);
|
|
62
|
+
if (index === -1)
|
|
63
|
+
return;
|
|
64
|
+
const wasOwner = state.claimants[0];
|
|
65
|
+
state.claimants.splice(index, 1);
|
|
66
|
+
if (state.claimants[0] !== wasOwner)
|
|
67
|
+
notify(state);
|
|
68
|
+
forget(scope, state);
|
|
69
|
+
}
|
|
70
|
+
/** Does `id` own `scope`'s cadence right now? */
|
|
71
|
+
export function isPollOwner(scope, id) {
|
|
72
|
+
const state = scopes.get(scope);
|
|
73
|
+
if (!state)
|
|
74
|
+
return false;
|
|
75
|
+
return state.claimants[0] === id;
|
|
76
|
+
}
|
|
77
|
+
/** Re-render me when this scope's owner changes. Returns the unsubscribe. */
|
|
78
|
+
export function subscribePollOwner(scope, listener) {
|
|
79
|
+
const state = stateFor(scope);
|
|
80
|
+
state.listeners.add(listener);
|
|
81
|
+
return () => {
|
|
82
|
+
state.listeners.delete(listener);
|
|
83
|
+
forget(scope, state);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Drop everything. Tests only. */
|
|
87
|
+
export function resetPollOwners() {
|
|
88
|
+
scopes.clear();
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=poll-owner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poll-owner.js","sourceRoot":"","sources":["../../../src/core/session/poll-owner.ts"],"names":[],"mappings":"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"}
|
|
@@ -29,4 +29,29 @@
|
|
|
29
29
|
export declare function ensurePreviewSessionCookie(previewUrl: string, options?: {
|
|
30
30
|
serverUrl?: string;
|
|
31
31
|
}): Promise<boolean>;
|
|
32
|
+
/**
|
|
33
|
+
* The URL a caller should actually open for `previewUrl`, having given it a
|
|
34
|
+
* credential the destination will accept.
|
|
35
|
+
*
|
|
36
|
+
* The two preview forms take credentials in different ways, and using the wrong
|
|
37
|
+
* one fails silently — which is exactly what happened before this existed:
|
|
38
|
+
* `ensurePreviewSessionCookie` returns false for an ORIGIN URL (there is no
|
|
39
|
+
* `/v1/p/` path to derive an auth endpoint from), so "open in a new tab" opened
|
|
40
|
+
* the origin with no cookie and no token, and the person landed on the sign-in
|
|
41
|
+
* gate instead of their file.
|
|
42
|
+
*
|
|
43
|
+
* - Preview ORIGIN (`{env}-p{port}-{sandbox}.{domain}`, or the localhost
|
|
44
|
+
* form): the API's `__preview_session` cookie is host-scoped and can never
|
|
45
|
+
* reach it, so the credential rides in the URL as a one-shot `?token`. The
|
|
46
|
+
* proxy exchanges it for a host cookie and strips it from the address bar.
|
|
47
|
+
* - Path proxy (`/v1/p/{sandbox}/{port}/…`): same origin as the API, so the
|
|
48
|
+
* cookie works; mint it and return the URL untouched.
|
|
49
|
+
*
|
|
50
|
+
* Never throws. With no token, or for a URL that is not a preview at all, the
|
|
51
|
+
* input is returned unchanged — the destination then answers for itself, which
|
|
52
|
+
* is more honest than fabricating a URL that cannot work.
|
|
53
|
+
*/
|
|
54
|
+
export declare function authorizePreviewUrl(previewUrl: string, options?: {
|
|
55
|
+
serverUrl?: string;
|
|
56
|
+
}): Promise<string>;
|
|
32
57
|
//# sourceMappingURL=preview-auth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preview-auth.d.ts","sourceRoot":"","sources":["../../../src/core/session/preview-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAKH;;;;;;;;GAQG;AACH,wBAAsB,0BAA0B,CAC9C,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACnC,OAAO,CAAC,OAAO,CAAC,CAoBlB"}
|
|
1
|
+
{"version":3,"file":"preview-auth.d.ts","sourceRoot":"","sources":["../../../src/core/session/preview-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAKH;;;;;;;;GAQG;AACH,wBAAsB,0BAA0B,CAC9C,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACnC,OAAO,CAAC,OAAO,CAAC,CAoBlB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACnC,OAAO,CAAC,MAAM,CAAC,CAOjB"}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* reloaded, bookmarked, or sent to anyone.
|
|
19
19
|
*/
|
|
20
20
|
import { getAuthToken } from '../http/auth.js';
|
|
21
|
-
import { buildPreviewAuthEndpoint } from './preview.js';
|
|
21
|
+
import { appendPreviewToken, buildPreviewAuthEndpoint, isSubdomainPreviewUrl } from './preview.js';
|
|
22
22
|
/**
|
|
23
23
|
* Mint the `__preview_session` cookie for `previewUrl`'s proxy origin.
|
|
24
24
|
*
|
|
@@ -50,4 +50,34 @@ export async function ensurePreviewSessionCookie(previewUrl, options = {}) {
|
|
|
50
50
|
return false;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The URL a caller should actually open for `previewUrl`, having given it a
|
|
55
|
+
* credential the destination will accept.
|
|
56
|
+
*
|
|
57
|
+
* The two preview forms take credentials in different ways, and using the wrong
|
|
58
|
+
* one fails silently — which is exactly what happened before this existed:
|
|
59
|
+
* `ensurePreviewSessionCookie` returns false for an ORIGIN URL (there is no
|
|
60
|
+
* `/v1/p/` path to derive an auth endpoint from), so "open in a new tab" opened
|
|
61
|
+
* the origin with no cookie and no token, and the person landed on the sign-in
|
|
62
|
+
* gate instead of their file.
|
|
63
|
+
*
|
|
64
|
+
* - Preview ORIGIN (`{env}-p{port}-{sandbox}.{domain}`, or the localhost
|
|
65
|
+
* form): the API's `__preview_session` cookie is host-scoped and can never
|
|
66
|
+
* reach it, so the credential rides in the URL as a one-shot `?token`. The
|
|
67
|
+
* proxy exchanges it for a host cookie and strips it from the address bar.
|
|
68
|
+
* - Path proxy (`/v1/p/{sandbox}/{port}/…`): same origin as the API, so the
|
|
69
|
+
* cookie works; mint it and return the URL untouched.
|
|
70
|
+
*
|
|
71
|
+
* Never throws. With no token, or for a URL that is not a preview at all, the
|
|
72
|
+
* input is returned unchanged — the destination then answers for itself, which
|
|
73
|
+
* is more honest than fabricating a URL that cannot work.
|
|
74
|
+
*/
|
|
75
|
+
export async function authorizePreviewUrl(previewUrl, options = {}) {
|
|
76
|
+
if (isSubdomainPreviewUrl(previewUrl)) {
|
|
77
|
+
const token = await getAuthToken();
|
|
78
|
+
return token ? appendPreviewToken(previewUrl, token) : previewUrl;
|
|
79
|
+
}
|
|
80
|
+
await ensurePreviewSessionCookie(previewUrl, options);
|
|
81
|
+
return previewUrl;
|
|
82
|
+
}
|
|
53
83
|
//# sourceMappingURL=preview-auth.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preview-auth.js","sourceRoot":"","sources":["../../../src/core/session/preview-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"preview-auth.js","sourceRoot":"","sources":["../../../src/core/session/preview-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAEhG;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,UAAkB,EAClB,UAAkC,EAAE;IAEpC,MAAM,YAAY,GAAG,wBAAwB,CAAC,UAAU,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IAC7E,IAAI,CAAC,YAAY;QAAE,OAAO,KAAK,CAAC;IAEhC,MAAM,KAAK,GAAG,MAAM,YAAY,EAAE,CAAC;IACnC,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IAEzB,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,YAAY,EAAE;YACzC,MAAM,EAAE,MAAM;YACd,WAAW,EAAE,SAAS;YACtB,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,KAAK,EAAE;gBAChC,cAAc,EAAE,kBAAkB;aACnC;SACF,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC,EAAE,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,UAAkB,EAClB,UAAkC,EAAE;IAEpC,IAAI,qBAAqB,CAAC,UAAU,CAAC,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,MAAM,YAAY,EAAE,CAAC;QACnC,OAAO,KAAK,CAAC,CAAC,CAAC,kBAAkB,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;IACpE,CAAC;IACD,MAAM,0BAA0B,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACtD,OAAO,UAAU,CAAC;AACpB,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cached template for a backend, or null when this deployment serves none —
|
|
3
|
+
* and also null before the first `load` resolves. Both mean "use the path form",
|
|
4
|
+
* which is why they need no distinction at the call site.
|
|
5
|
+
*/
|
|
6
|
+
export declare function cachedPreviewUrlTemplate(backendUrl: string): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Whether this backend has ANSWERED yet — as opposed to "answered null", which
|
|
9
|
+
* is a real answer meaning "this deployment serves no preview domain".
|
|
10
|
+
*
|
|
11
|
+
* The two look identical through `cachedPreviewUrlTemplate`, and conflating them
|
|
12
|
+
* is how a session gets stuck: during a rolling deploy the first `ensureReady()`
|
|
13
|
+
* can hit a task that predates `/v1/p/config`, and without this the handle would
|
|
14
|
+
* keep building path-proxy URLs for the rest of its life even though every later
|
|
15
|
+
* request would have been answered by a new task.
|
|
16
|
+
*/
|
|
17
|
+
export declare function hasPreviewConfig(backendUrl: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Fetch and cache the template. Concurrent callers share one request; a failed
|
|
20
|
+
* fetch caches nothing, so a deployment that was briefly unreachable is asked
|
|
21
|
+
* again rather than being remembered as having no preview domain.
|
|
22
|
+
*/
|
|
23
|
+
export declare function loadPreviewUrlTemplate(backendUrl: string): Promise<string | null>;
|
|
24
|
+
/** Drop everything remembered — for tests and for a host re-pointing its API. */
|
|
25
|
+
export declare function resetPreviewConfigCache(): void;
|
|
26
|
+
//# sourceMappingURL=preview-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview-config.d.ts","sourceRoot":"","sources":["../../../src/core/session/preview-config.ts"],"names":[],"mappings":"AAiCA;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE1E;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgCvF;AAED,iFAAiF;AACjF,wBAAgB,uBAAuB,IAAI,IAAI,CAG9C"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How this deployment addresses previews, fetched once from `GET /v1/p/config`.
|
|
3
|
+
*
|
|
4
|
+
* The preview hostname shape (`{env}-p{port}-{sandbox}.{domain}`) is a property
|
|
5
|
+
* of the deployment, not of the client: it depends on the wildcard domain the
|
|
6
|
+
* operator serves and on which environment the API is. Re-deriving it here from
|
|
7
|
+
* `backendUrl` would mean two implementations of one rule, which is how they
|
|
8
|
+
* drift. So the API states it as a template and this caches the answer.
|
|
9
|
+
*
|
|
10
|
+
* `previewUrl()` is synchronous — a React render calls it — so the fetch cannot
|
|
11
|
+
* happen there. `ensureReady()` warms this first, and until it resolves callers
|
|
12
|
+
* get the path form, which still works for everything that is not a browser.
|
|
13
|
+
*/
|
|
14
|
+
import { backendApi } from '../http/api-client.js';
|
|
15
|
+
const templates = new Map();
|
|
16
|
+
const inFlight = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* Cache key: the backend URL without trailing slashes, stripped LINEARLY. The
|
|
19
|
+
* regex form (`/\/+$/`) backtracks quadratically on adversarial input (CodeQL
|
|
20
|
+
* js/polynomial-redos), and this runs on a host-supplied string.
|
|
21
|
+
*/
|
|
22
|
+
function key(backendUrl) {
|
|
23
|
+
let end = backendUrl.length;
|
|
24
|
+
while (end > 0 && backendUrl.charCodeAt(end - 1) === 47 /* '/' */)
|
|
25
|
+
end--;
|
|
26
|
+
return backendUrl.slice(0, end);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The cached template for a backend, or null when this deployment serves none —
|
|
30
|
+
* and also null before the first `load` resolves. Both mean "use the path form",
|
|
31
|
+
* which is why they need no distinction at the call site.
|
|
32
|
+
*/
|
|
33
|
+
export function cachedPreviewUrlTemplate(backendUrl) {
|
|
34
|
+
return templates.get(key(backendUrl)) ?? null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Whether this backend has ANSWERED yet — as opposed to "answered null", which
|
|
38
|
+
* is a real answer meaning "this deployment serves no preview domain".
|
|
39
|
+
*
|
|
40
|
+
* The two look identical through `cachedPreviewUrlTemplate`, and conflating them
|
|
41
|
+
* is how a session gets stuck: during a rolling deploy the first `ensureReady()`
|
|
42
|
+
* can hit a task that predates `/v1/p/config`, and without this the handle would
|
|
43
|
+
* keep building path-proxy URLs for the rest of its life even though every later
|
|
44
|
+
* request would have been answered by a new task.
|
|
45
|
+
*/
|
|
46
|
+
export function hasPreviewConfig(backendUrl) {
|
|
47
|
+
return templates.has(key(backendUrl));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Fetch and cache the template. Concurrent callers share one request; a failed
|
|
51
|
+
* fetch caches nothing, so a deployment that was briefly unreachable is asked
|
|
52
|
+
* again rather than being remembered as having no preview domain.
|
|
53
|
+
*/
|
|
54
|
+
export async function loadPreviewUrlTemplate(backendUrl) {
|
|
55
|
+
const cacheKey = key(backendUrl);
|
|
56
|
+
const cached = templates.get(cacheKey);
|
|
57
|
+
if (cached !== undefined)
|
|
58
|
+
return cached;
|
|
59
|
+
const pending = inFlight.get(cacheKey);
|
|
60
|
+
if (pending)
|
|
61
|
+
return pending;
|
|
62
|
+
const request = (async () => {
|
|
63
|
+
try {
|
|
64
|
+
const response = await backendApi.get('/p/config', {
|
|
65
|
+
showErrors: false,
|
|
66
|
+
// A deployment without a preview domain, and an API too old to know the
|
|
67
|
+
// route, are both ordinary answers — not incidents to report.
|
|
68
|
+
errorContext: { operation: 'load preview config', silent: true },
|
|
69
|
+
});
|
|
70
|
+
if (!response.success)
|
|
71
|
+
return null;
|
|
72
|
+
const template = typeof response.data?.preview_url_template === 'string'
|
|
73
|
+
? response.data.preview_url_template
|
|
74
|
+
: null;
|
|
75
|
+
templates.set(cacheKey, template);
|
|
76
|
+
return template;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// An older API has no such route, and a network blip is not an answer.
|
|
80
|
+
// Leave the cache empty so the next ensureReady() asks again.
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
inFlight.delete(cacheKey);
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
inFlight.set(cacheKey, request);
|
|
88
|
+
return request;
|
|
89
|
+
}
|
|
90
|
+
/** Drop everything remembered — for tests and for a host re-pointing its API. */
|
|
91
|
+
export function resetPreviewConfigCache() {
|
|
92
|
+
templates.clear();
|
|
93
|
+
inFlight.clear();
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=preview-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview-config.js","sourceRoot":"","sources":["../../../src/core/session/preview-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAMhD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAyB,CAAC;AACnD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkC,CAAC;AAE3D;;;;GAIG;AACH,SAAS,GAAG,CAAC,UAAkB;IAC7B,IAAI,GAAG,GAAG,UAAU,CAAC,MAAM,CAAC;IAC5B,OAAO,GAAG,GAAG,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,SAAS;QAAE,GAAG,EAAE,CAAC;IACzE,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAClC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,UAAkB;IACzD,OAAO,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,IAAI,CAAC;AAChD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAkB;IACjD,OAAO,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC;AACxC,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,UAAkB;IAC7D,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC;IACjC,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACvC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IACxC,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACvC,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAE5B,MAAM,OAAO,GAAG,CAAC,KAAK,IAAI,EAAE;QAC1B,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,GAAG,CAAwB,WAAW,EAAE;gBACxE,UAAU,EAAE,KAAK;gBACjB,wEAAwE;gBACxE,8DAA8D;gBAC9D,YAAY,EAAE,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,EAAE,IAAI,EAAE;aACjE,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,OAAO;gBAAE,OAAO,IAAI,CAAC;YACnC,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,EAAE,oBAAoB,KAAK,QAAQ;gBACtE,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,oBAAoB;gBACpC,CAAC,CAAC,IAAI,CAAC;YACT,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;YAClC,OAAO,QAAQ,CAAC;QAClB,CAAC;QAAC,MAAM,CAAC;YACP,uEAAuE;YACvE,8DAA8D;YAC9D,OAAO,IAAI,CAAC;QACd,CAAC;gBAAS,CAAC;YACT,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;IAEL,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAChC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,uBAAuB;IACrC,SAAS,CAAC,KAAK,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,EAAE,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SubdomainUrlOptions } from './url.js';
|
|
2
|
+
/**
|
|
3
|
+
* A fully-resolved options bag. Every field required — including
|
|
4
|
+
* `previewUrlTemplate`, whose `null` means "this deployment serves no preview
|
|
5
|
+
* domain" and is a real answer, not an omission.
|
|
6
|
+
*/
|
|
7
|
+
export interface ResolvedPreviewOptions extends SubdomainUrlOptions {
|
|
8
|
+
previewUrlTemplate: string | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolve preview options for a sandbox against a backend.
|
|
12
|
+
*
|
|
13
|
+
* Reads the deployment's answer from the `GET /v1/p/config` cache, and asks
|
|
14
|
+
* again in the background when this backend has never answered — a rolling
|
|
15
|
+
* deploy can serve one request from a task that predates the route, and without
|
|
16
|
+
* the retry that single miss would pin the caller to the path proxy for its
|
|
17
|
+
* whole life. A backend that answered "no preview domain" HAS answered and is
|
|
18
|
+
* not re-asked.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolvePreviewOptions(input: {
|
|
21
|
+
sandboxId: string;
|
|
22
|
+
apiBaseUrl: string;
|
|
23
|
+
backendPort: number;
|
|
24
|
+
}): ResolvedPreviewOptions;
|
|
25
|
+
//# sourceMappingURL=preview-options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview-options.d.ts","sourceRoot":"","sources":["../../../src/core/session/preview-options.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAEjD;;;;GAIG;AACH,MAAM,WAAW,sBAAuB,SAAQ,mBAAmB;IACjE,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,sBAAsB,CASzB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE way to build the options a preview URL is derived from.
|
|
3
|
+
*
|
|
4
|
+
* `rewriteLocalhostUrl` and its siblings in `url.ts` are pure: they answer
|
|
5
|
+
* "which URL" from an options bag and never touch the network. That purity is
|
|
6
|
+
* worth keeping — it is why they are exhaustively testable — but it puts the
|
|
7
|
+
* burden of assembling a COMPLETE bag on whoever calls them, and an incomplete
|
|
8
|
+
* bag does not fail. It falls back to the path proxy, silently, forever.
|
|
9
|
+
*
|
|
10
|
+
* That is not hypothetical. There are two call paths — a session handle
|
|
11
|
+
* (`kortix.session(...).previewUrl()`) and the ambient runtime
|
|
12
|
+
* (`deriveSubdomainOpts`, which the web app's preview iframe, `useSandboxProxy`
|
|
13
|
+
* and `buildStaticFilePreviewUrl` all use) — and each assembled the bag itself.
|
|
14
|
+
* When `previewUrlTemplate` was added, only the first was updated, so every
|
|
15
|
+
* preview in the session panel kept rendering `/v1/p/{sandbox}/{port}` however
|
|
16
|
+
* loudly the deployment advertised an origin. Nothing failed; the wrong thing
|
|
17
|
+
* merely kept working.
|
|
18
|
+
*
|
|
19
|
+
* So: one producer, and a return type whose fields are all REQUIRED. A future
|
|
20
|
+
* call path that forgets a field does not compile, which is the only kind of
|
|
21
|
+
* guarantee worth having for a failure mode this quiet.
|
|
22
|
+
*/
|
|
23
|
+
import { cachedPreviewUrlTemplate, hasPreviewConfig, loadPreviewUrlTemplate } from './preview-config.js';
|
|
24
|
+
/**
|
|
25
|
+
* Resolve preview options for a sandbox against a backend.
|
|
26
|
+
*
|
|
27
|
+
* Reads the deployment's answer from the `GET /v1/p/config` cache, and asks
|
|
28
|
+
* again in the background when this backend has never answered — a rolling
|
|
29
|
+
* deploy can serve one request from a task that predates the route, and without
|
|
30
|
+
* the retry that single miss would pin the caller to the path proxy for its
|
|
31
|
+
* whole life. A backend that answered "no preview domain" HAS answered and is
|
|
32
|
+
* not re-asked.
|
|
33
|
+
*/
|
|
34
|
+
export function resolvePreviewOptions(input) {
|
|
35
|
+
const { apiBaseUrl } = input;
|
|
36
|
+
if (!hasPreviewConfig(apiBaseUrl))
|
|
37
|
+
void loadPreviewUrlTemplate(apiBaseUrl).catch(() => { });
|
|
38
|
+
return {
|
|
39
|
+
sandboxId: input.sandboxId,
|
|
40
|
+
backendPort: input.backendPort,
|
|
41
|
+
apiBaseUrl,
|
|
42
|
+
previewUrlTemplate: cachedPreviewUrlTemplate(apiBaseUrl),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=preview-options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview-options.js","sourceRoot":"","sources":["../../../src/core/session/preview-options.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAYtG;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAIrC;IACC,MAAM,EAAE,UAAU,EAAE,GAAG,KAAK,CAAC;IAC7B,IAAI,CAAC,gBAAgB,CAAC,UAAU,CAAC;QAAE,KAAK,sBAAsB,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC3F,OAAO;QACL,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,UAAU;QACV,kBAAkB,EAAE,wBAAwB,CAAC,UAAU,CAAC;KACzD,CAAC;AACJ,CAAC"}
|
|
@@ -14,9 +14,14 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function buildPreviewAuthEndpoint(previewUrl: string, serverUrl?: string): string | null;
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Preview ORIGIN form: `p{port}-{sandbox}.{host}` locally, or the deployed
|
|
18
|
+
* `{env}-p{port}-{sandbox}.{domain}`.
|
|
19
|
+
*
|
|
20
|
+
* These can't use the host-only `/v1/p/` session cookie — it never reaches
|
|
21
|
+
* another hostname — so they authenticate the FIRST request with a one-shot
|
|
22
|
+
* `?token`, which the proxy exchanges for a cookie on the preview origin and
|
|
23
|
+
* then strips. Every later request (sub-resources, `fetch`, WebSockets) rides
|
|
24
|
+
* that cookie, which is the only credential an app's own code can carry.
|
|
20
25
|
*/
|
|
21
26
|
export declare function isSubdomainPreviewUrl(candidateUrl: string): boolean;
|
|
22
27
|
/** Append a one-shot `?token` to a (subdomain) preview URL. */
|