@opengeni/core 0.28.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/README.md +1 -1
- package/dist/access/external-actor-authority.d.ts +47 -0
- package/dist/access/index.d.ts +182 -3
- package/dist/access/service-initiator.d.ts +3 -0
- package/dist/application/api-integration-servers.d.ts +20 -0
- package/dist/application/archived-session-imports.d.ts +6 -0
- package/dist/application/composer-submit.d.ts +24 -0
- package/dist/application/connect-authority.d.ts +13 -0
- package/dist/application/connect-operation.d.ts +31 -0
- package/dist/application/external-continuation.d.ts +15 -0
- package/dist/application/external-identity-lifecycle.d.ts +20 -0
- package/dist/application/external-link-work-admission.d.ts +12 -0
- package/dist/application/external-workspace-members.d.ts +34 -0
- package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
- package/dist/application/new-session-drafts.d.ts +39 -2
- package/dist/application/sandbox-recovery.d.ts +58 -0
- package/dist/application/session-commands.d.ts +26 -13
- package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
- package/dist/application/session-tenancy.d.ts +19 -0
- package/dist/application/user-resource-grants.d.ts +20 -0
- package/dist/billing/limits.d.ts +11 -0
- package/dist/canonical-human-identities.d.ts +7 -2
- package/dist/canonical-human-identities.js +17 -4
- package/dist/canonical-human-identities.js.map +1 -1
- package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
- package/dist/chunk-2MKMRGSY.js.map +1 -0
- package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
- package/dist/chunk-GVO3N37G.js.map +1 -0
- package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
- package/dist/chunk-LCQXNR62.js.map +1 -0
- package/dist/chunk-MM6N7CTO.js +387 -0
- package/dist/chunk-MM6N7CTO.js.map +1 -0
- package/dist/chunk-QO5GVFFO.js +235 -0
- package/dist/chunk-QO5GVFFO.js.map +1 -0
- package/dist/codex-model-availability.d.ts +14 -0
- package/dist/default-session-model.d.ts +139 -0
- package/dist/dependencies.d.ts +87 -4
- package/dist/developer-setup-credential-ceiling.d.ts +10 -0
- package/dist/domain/agent-config-resolution.d.ts +49 -0
- package/dist/domain/automations.d.ts +58 -0
- package/dist/domain/capabilities.d.ts +22 -4
- package/dist/domain/company-profile-agent-admin.d.ts +25 -0
- package/dist/domain/connection-account-selection-error.d.ts +8 -0
- package/dist/domain/connector-tool-permissions.d.ts +36 -0
- package/dist/domain/direct-model-provider.d.ts +3 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
- package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
- package/dist/domain/editable-artifacts/ports.d.ts +10 -0
- package/dist/domain/editable-artifacts/service.d.ts +4 -3
- package/dist/domain/environments.d.ts +5 -9
- package/dist/domain/external-creation-attribution.d.ts +6 -0
- package/dist/domain/file-owner.d.ts +8 -0
- package/dist/domain/github-action-policies.d.ts +36 -0
- package/dist/domain/github-repository-bindings.d.ts +117 -0
- package/dist/domain/github-skill-source.d.ts +5 -0
- package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
- package/dist/domain/insights-timing.d.ts +16 -0
- package/dist/domain/insights-usage.d.ts +27 -0
- package/dist/domain/insights.d.ts +28 -1
- package/dist/domain/knowledge-files.d.ts +20 -0
- package/dist/domain/knowledge-messages.d.ts +39 -0
- package/dist/domain/knowledge-messages.test.d.ts +1 -0
- package/dist/domain/knowledge-preparation.d.ts +10 -0
- package/dist/domain/knowledge-preparation.test.d.ts +1 -0
- package/dist/domain/knowledge-search.d.ts +15 -0
- package/dist/domain/knowledge-search.test.d.ts +1 -0
- package/dist/domain/knowledge.d.ts +9 -0
- package/dist/domain/mcp-account-bindings.d.ts +19 -0
- package/dist/domain/mcp-account-routes.d.ts +21 -0
- package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
- package/dist/domain/organization-integration-catalog.d.ts +15 -0
- package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
- package/dist/domain/personal-connection-delegations.d.ts +87 -4
- package/dist/domain/pr-review.d.ts +249 -0
- package/dist/domain/resources.d.ts +24 -3
- package/dist/domain/scheduled-task-access.d.ts +247 -0
- package/dist/domain/scheduled-tasks.d.ts +103 -6
- package/dist/domain/session-connection-accounts.d.ts +16 -0
- package/dist/domain/session-file-access.d.ts +4 -0
- package/dist/domain/session-native-connection-replacement.d.ts +14 -0
- package/dist/domain/session-start-timing.d.ts +7 -0
- package/dist/domain/session-tool-policy.d.ts +93 -4
- package/dist/domain/sessions.d.ts +275 -28
- package/dist/domain/skill-imports.d.ts +1 -0
- package/dist/domain/skill-search.d.ts +30 -0
- package/dist/domain/skills.d.ts +117 -0
- package/dist/domain/slack-bot.d.ts +46 -1
- package/dist/domain/workspace-members.d.ts +8 -1
- package/dist/editable-artifact-live/application.d.ts +12 -3
- package/dist/editable-artifact-live/ports.d.ts +0 -1
- package/dist/editable-artifact-live/server.d.ts +5 -2
- package/dist/editable-artifact-live/ticket.d.ts +2 -2
- package/dist/editable-artifact-live/types.d.ts +14 -2
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +10 -4
- package/dist/index.d.ts +49 -3
- package/dist/index.js +18812 -6833
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.d.ts +131 -0
- package/dist/managed-auth-session-sets.js +51 -0
- package/dist/managed-auth-session-sets.js.map +1 -0
- package/dist/managed-session.d.ts +98 -11
- package/dist/model-catalog.d.ts +117 -0
- package/dist/product-usage-metrics.d.ts +30 -0
- package/dist/rigs/index.d.ts +22 -5
- package/dist/rigs/provider-images.d.ts +1 -1
- package/dist/sandbox/fleet.d.ts +59 -10
- package/dist/sandbox/routing.d.ts +22 -2
- package/dist/sandbox/runtime-settings.d.ts +56 -0
- package/dist/session-authorization.d.ts +38 -1
- package/dist/site-session-origin.d.ts +9 -0
- package/dist/transcription.d.ts +12 -1
- package/dist/turn-surface.d.ts +18 -0
- package/dist/user-presence.d.ts +32 -0
- package/package.json +18 -11
- package/src/access/external-actor-authority.ts +94 -0
- package/src/access/index.ts +1386 -44
- package/src/access/service-initiator.ts +50 -0
- package/src/application/api-integration-servers.ts +215 -0
- package/src/application/archived-session-imports.ts +161 -0
- package/src/application/composer-submit.ts +77 -0
- package/src/application/connect-authority.ts +77 -0
- package/src/application/connect-operation.ts +80 -0
- package/src/application/external-continuation.ts +129 -0
- package/src/application/external-identity-lifecycle.ts +48 -0
- package/src/application/external-link-work-admission.ts +90 -0
- package/src/application/external-workspace-members.ts +232 -0
- package/src/application/modal-native-worker-host-transport.ts +263 -0
- package/src/application/new-session-drafts.ts +222 -29
- package/src/application/sandbox-recovery.ts +78 -0
- package/src/application/session-commands.ts +430 -73
- package/src/application/session-mcp-credential-rotation.ts +217 -0
- package/src/application/session-tenancy.ts +445 -0
- package/src/application/user-resource-grants.ts +154 -0
- package/src/billing/limits.ts +103 -24
- package/src/canonical-human-identities.ts +22 -3
- package/src/codex-model-availability.ts +136 -0
- package/src/default-session-model.ts +656 -0
- package/src/dependencies.ts +97 -5
- package/src/developer-setup-credential-ceiling.ts +60 -0
- package/src/domain/agent-config-resolution.ts +148 -0
- package/src/domain/automations.ts +270 -0
- package/src/domain/capabilities.ts +244 -151
- package/src/domain/company-profile-agent-admin.ts +56 -0
- package/src/domain/connection-account-selection-error.ts +19 -0
- package/src/domain/connector-tool-permissions.ts +371 -0
- package/src/domain/direct-model-provider.ts +62 -0
- package/src/domain/editable-artifacts/agent-application.ts +61 -15
- package/src/domain/editable-artifacts/durable-export.ts +100 -5
- package/src/domain/editable-artifacts/in-memory.ts +6 -0
- package/src/domain/editable-artifacts/ports.ts +11 -0
- package/src/domain/editable-artifacts/service.ts +32 -1
- package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
- package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
- package/src/domain/environments.ts +25 -45
- package/src/domain/external-creation-attribution.ts +22 -0
- package/src/domain/file-owner.ts +67 -0
- package/src/domain/github-action-policies.ts +169 -0
- package/src/domain/github-repository-bindings.ts +295 -0
- package/src/domain/github-skill-source.ts +233 -0
- package/src/domain/governed-learning-slack-publication.ts +157 -0
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights-usage.ts +67 -0
- package/src/domain/insights.ts +270 -169
- package/src/domain/knowledge-files.ts +46 -0
- package/src/domain/knowledge-messages.test.ts +91 -0
- package/src/domain/knowledge-messages.ts +106 -0
- package/src/domain/knowledge-preparation.test.ts +276 -0
- package/src/domain/knowledge-preparation.ts +147 -0
- package/src/domain/knowledge-search.test.ts +561 -0
- package/src/domain/knowledge-search.ts +256 -0
- package/src/domain/knowledge.ts +133 -0
- package/src/domain/mcp-account-bindings.ts +171 -0
- package/src/domain/mcp-account-routes.ts +121 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +88 -0
- package/src/domain/organization-membership-lifecycle.ts +10 -0
- package/src/domain/personal-connection-delegations.ts +758 -115
- package/src/domain/pr-review.ts +687 -0
- package/src/domain/resources.ts +182 -25
- package/src/domain/scheduled-task-access.ts +1052 -0
- package/src/domain/scheduled-tasks.ts +1952 -215
- package/src/domain/session-connection-accounts.ts +67 -0
- package/src/domain/session-file-access.ts +47 -0
- package/src/domain/session-native-connection-replacement.ts +64 -0
- package/src/domain/session-start-timing.ts +55 -0
- package/src/domain/session-tool-policy.ts +443 -19
- package/src/domain/sessions.ts +3417 -619
- package/src/domain/skill-imports.ts +137 -17
- package/src/domain/skill-search.ts +221 -0
- package/src/domain/skills.ts +104 -0
- package/src/domain/slack-bot.ts +128 -3
- package/src/domain/workspace-members.ts +34 -2
- package/src/editable-artifact-live/application.ts +29 -4
- package/src/editable-artifact-live/ports.ts +0 -1
- package/src/editable-artifact-live/server.ts +167 -13
- package/src/editable-artifact-live/ticket.ts +87 -6
- package/src/editable-artifact-live/types.ts +16 -2
- package/src/editable-artifact-live/wire.ts +0 -10
- package/src/index.ts +69 -4
- package/src/managed-auth-session-sets.ts +371 -0
- package/src/managed-session.ts +520 -2
- package/src/model-catalog.ts +873 -0
- package/src/product-usage-metrics.ts +101 -0
- package/src/rigs/index.ts +179 -63
- package/src/rigs/provider-images.ts +2 -3
- package/src/sandbox/fleet.ts +471 -59
- package/src/sandbox/routing.ts +160 -33
- package/src/sandbox/runtime-settings.ts +281 -0
- package/src/session-authorization.ts +262 -78
- package/src/site-session-origin.ts +22 -0
- package/src/transcription.ts +13 -1
- package/src/turn-surface.ts +45 -0
- package/src/user-presence.ts +134 -0
- package/dist/chunk-6WKPWE5S.js.map +0 -1
- package/dist/chunk-IBOEYG6N.js +0 -34
- package/dist/chunk-IBOEYG6N.js.map +0 -1
- package/dist/chunk-JJU6I5XD.js.map +0 -1
- package/dist/chunk-NYPMQ7NO.js.map +0 -1
- package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
- package/dist/domain/packs.d.ts +0 -55
- package/src/domain/durable-learning-slack-publication.ts +0 -202
- package/src/domain/packs.ts +0 -556
package/src/domain/slack-bot.ts
CHANGED
|
@@ -3,10 +3,14 @@ import {
|
|
|
3
3
|
OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
4
4
|
OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
|
|
5
5
|
OpenGeniSlackBotConnectionMetadata,
|
|
6
|
+
SCHEDULED_SLACK_BOT_POSTING_TOOLS,
|
|
6
7
|
areOpenGeniSlackBotScopesAccepted,
|
|
7
8
|
type AccessGrant,
|
|
8
9
|
type ConnectionMetadata,
|
|
10
|
+
type FirstPartyMcpToolName,
|
|
9
11
|
type OpenGeniSlackBotConnectionMetadata as OpenGeniSlackBotMetadata,
|
|
12
|
+
type ScheduledTask,
|
|
13
|
+
type ScheduledTaskAgentConfig,
|
|
10
14
|
type Session,
|
|
11
15
|
} from "@opengeni/contracts";
|
|
12
16
|
import {
|
|
@@ -15,7 +19,8 @@ import {
|
|
|
15
19
|
type Database,
|
|
16
20
|
} from "@opengeni/db";
|
|
17
21
|
import { HTTPException } from "hono/http-exception";
|
|
18
|
-
import { requirePermission } from "../access";
|
|
22
|
+
import { requirePermission, type AccessGrantAuthorization } from "../access";
|
|
23
|
+
import { grantHasAgentAttemptAuthority } from "../session-authorization";
|
|
19
24
|
|
|
20
25
|
export function openGeniSlackBotMetadata(
|
|
21
26
|
metadata: Record<string, unknown>,
|
|
@@ -72,12 +77,12 @@ export async function requireOpenGeniSlackBotConnection(
|
|
|
72
77
|
const connection = await getConnectionMetadata(db, workspaceId, connectionId, null);
|
|
73
78
|
if (!connection || !isOpenGeniSlackBotConnection(connection)) {
|
|
74
79
|
throw new HTTPException(422, {
|
|
75
|
-
message: "slackBotConnectionId must reference an
|
|
80
|
+
message: "slackBotConnectionId must reference an Opengeni Slack bot connection",
|
|
76
81
|
});
|
|
77
82
|
}
|
|
78
83
|
if (connection.status !== "active") {
|
|
79
84
|
throw new HTTPException(422, {
|
|
80
|
-
message: `
|
|
85
|
+
message: `Opengeni Slack bot connection is not active (${connection.status})`,
|
|
81
86
|
});
|
|
82
87
|
}
|
|
83
88
|
return connection;
|
|
@@ -126,3 +131,123 @@ export function isTrustedScheduledSlackBotSession(
|
|
|
126
131
|
function zUuid(value: string): boolean {
|
|
127
132
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
128
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Proves, with the bot's own token, that it may post in the chosen channel:
|
|
137
|
+
* the bot is a member and the channel is active and not shared with another
|
|
138
|
+
* organization. Supplied only by the HTTP routes a person uses to edit tasks.
|
|
139
|
+
*/
|
|
140
|
+
export type ScheduledTaskSlackChannelVerifier = (input: {
|
|
141
|
+
connectionId: string;
|
|
142
|
+
channelId: string;
|
|
143
|
+
}) => Promise<void>;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Whether this request is a signed-in person acting for themselves: never an
|
|
147
|
+
* agent attempt, a service, an API key, or a request carrying service
|
|
148
|
+
* provenance. The same rule gates Agent learning settings.
|
|
149
|
+
*/
|
|
150
|
+
export function isAuthenticatedPersonAuthorization(
|
|
151
|
+
authorization: AccessGrantAuthorization | undefined,
|
|
152
|
+
): boolean {
|
|
153
|
+
if (!authorization) return false;
|
|
154
|
+
const { grant } = authorization;
|
|
155
|
+
return (
|
|
156
|
+
grant.principalKind === "human_session" &&
|
|
157
|
+
authorization.contextIntegrity &&
|
|
158
|
+
authorization.authenticatedSubjectId === grant.subjectId &&
|
|
159
|
+
!grant.serviceInitiator &&
|
|
160
|
+
!grant.serviceInitiatorContext &&
|
|
161
|
+
grant.metadata?.attemptId === undefined &&
|
|
162
|
+
grant.metadata?.turnId === undefined &&
|
|
163
|
+
grant.metadata?.executionGeneration === undefined &&
|
|
164
|
+
!grantHasAgentAttemptAuthority(grant)
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* A scheduled task may post as the OpenGeni workspace bot only to the one
|
|
170
|
+
* Slack channel a person chose on the task. Choosing or changing that channel
|
|
171
|
+
* therefore needs a signed-in person with `connections:write`, and the bot's
|
|
172
|
+
* membership is verified at that moment. Keeping or clearing the existing
|
|
173
|
+
* channel needs neither, so an agent editing other task fields cannot redirect
|
|
174
|
+
* posts but also does not have to re-prove the person's choice. Clearing is the
|
|
175
|
+
* safe direction and is never refused, even for a task that continues one chat.
|
|
176
|
+
*/
|
|
177
|
+
export async function validateScheduledTaskSlackChannel(input: {
|
|
178
|
+
grant: AccessGrant;
|
|
179
|
+
authorization?: AccessGrantAuthorization | undefined;
|
|
180
|
+
previous: Pick<ScheduledTaskAgentConfig, "slackBotConnectionId" | "slackBotChannelId"> | null;
|
|
181
|
+
next: Pick<ScheduledTaskAgentConfig, "slackBotConnectionId" | "slackBotChannelId">;
|
|
182
|
+
runMode: ScheduledTask["runMode"];
|
|
183
|
+
/**
|
|
184
|
+
* Null unless the task already created the reusable chat its runs continue.
|
|
185
|
+
* Otherwise reports whether that chat was created with the posting tools,
|
|
186
|
+
* which are fixed at creation.
|
|
187
|
+
*/
|
|
188
|
+
reusableSessionCanPost: (() => Promise<boolean>) | null;
|
|
189
|
+
verifySlackChannel?: ScheduledTaskSlackChannelVerifier | undefined;
|
|
190
|
+
}): Promise<void> {
|
|
191
|
+
const previousChannel = input.previous?.slackBotChannelId ?? null;
|
|
192
|
+
const nextChannel = input.next.slackBotChannelId ?? null;
|
|
193
|
+
// Clearing takes effect at once: the tools re-read the task and refuse.
|
|
194
|
+
if (!nextChannel) return;
|
|
195
|
+
if (!input.next.slackBotConnectionId) {
|
|
196
|
+
throw new HTTPException(422, {
|
|
197
|
+
message: "slackBotChannelId requires slackBotConnectionId",
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (input.runMode === "existing_session") {
|
|
201
|
+
throw new HTTPException(422, {
|
|
202
|
+
message: "Slack channel posting needs a task that starts its own chats",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (
|
|
206
|
+
previousChannel === nextChannel &&
|
|
207
|
+
(input.previous?.slackBotConnectionId ?? null) === input.next.slackBotConnectionId
|
|
208
|
+
) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (!isAuthenticatedPersonAuthorization(input.authorization)) {
|
|
212
|
+
throw new HTTPException(403, {
|
|
213
|
+
message: "Only a person can choose the Slack channel a scheduled task posts to",
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
requirePermission(input.grant, "connections:write");
|
|
217
|
+
// A chat created without the posting tools keeps that tool set, so choosing
|
|
218
|
+
// a channel for it would silently post nothing.
|
|
219
|
+
if (input.reusableSessionCanPost && !(await input.reusableSessionCanPost())) {
|
|
220
|
+
throw new HTTPException(409, {
|
|
221
|
+
message:
|
|
222
|
+
"this task continues a chat that was created without Slack posting; recreate the task to post from it",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (!input.verifySlackChannel) {
|
|
226
|
+
throw new HTTPException(422, {
|
|
227
|
+
message: "Choose the Slack channel in the schedule editor",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
await input.verifySlackChannel({
|
|
231
|
+
connectionId: input.next.slackBotConnectionId,
|
|
232
|
+
channelId: nextChannel,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Add the bot posting tools to a generated scheduled run when, and only when,
|
|
238
|
+
* a person chose the task's Slack channel. The deployment ceiling still
|
|
239
|
+
* applies: an operator who disallowed these tools keeps them off.
|
|
240
|
+
*/
|
|
241
|
+
export function withScheduledSlackBotPostingTools(
|
|
242
|
+
tools: readonly FirstPartyMcpToolName[],
|
|
243
|
+
agentConfig: Pick<ScheduledTaskAgentConfig, "slackBotConnectionId" | "slackBotChannelId">,
|
|
244
|
+
allowedTools: readonly FirstPartyMcpToolName[],
|
|
245
|
+
): FirstPartyMcpToolName[] {
|
|
246
|
+
if (!agentConfig.slackBotConnectionId || !agentConfig.slackBotChannelId) return [...tools];
|
|
247
|
+
const allowed = new Set(allowedTools);
|
|
248
|
+
const next = [...tools];
|
|
249
|
+
for (const tool of SCHEDULED_SLACK_BOT_POSTING_TOOLS) {
|
|
250
|
+
if (allowed.has(tool) && !next.includes(tool)) next.push(tool);
|
|
251
|
+
}
|
|
252
|
+
return next;
|
|
253
|
+
}
|
|
@@ -44,7 +44,9 @@ export function assertWorkspaceMemberRemovable(input: {
|
|
|
44
44
|
}): void {
|
|
45
45
|
const { members, subjectId, callerSubjectId } = input;
|
|
46
46
|
if (subjectId === callerSubjectId) {
|
|
47
|
-
throw new HTTPException(409, {
|
|
47
|
+
throw new HTTPException(409, {
|
|
48
|
+
message: "you cannot remove your own membership",
|
|
49
|
+
});
|
|
48
50
|
}
|
|
49
51
|
const target = members.find((member) => member.subjectId === subjectId);
|
|
50
52
|
if (!target) {
|
|
@@ -62,6 +64,34 @@ export function assertWorkspaceMemberRemovable(input: {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
/** Keep scoped member edits from orphaning the workspace or changing the caller's own grant. */
|
|
68
|
+
export function assertWorkspaceMemberUpdateAllowed(input: {
|
|
69
|
+
members: WorkspaceMember[];
|
|
70
|
+
subjectId: string;
|
|
71
|
+
callerSubjectId: string;
|
|
72
|
+
nextPermissions: Permission[];
|
|
73
|
+
}): void {
|
|
74
|
+
const { members, subjectId, callerSubjectId, nextPermissions } = input;
|
|
75
|
+
if (subjectId === callerSubjectId) {
|
|
76
|
+
throw new HTTPException(409, {
|
|
77
|
+
message: "you cannot change your own workspace access",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const target = members.find((member) => member.subjectId === subjectId);
|
|
81
|
+
if (!target) {
|
|
82
|
+
throw new HTTPException(404, { message: "member not found" });
|
|
83
|
+
}
|
|
84
|
+
if (
|
|
85
|
+
memberCanAdminister(target) &&
|
|
86
|
+
!memberCanAdminister({ permissions: nextPermissions }) &&
|
|
87
|
+
!members.some((member) => member.subjectId !== subjectId && memberCanAdminister(member))
|
|
88
|
+
) {
|
|
89
|
+
throw new HTTPException(409, {
|
|
90
|
+
message: "the workspace must keep at least one administrator",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
65
95
|
/**
|
|
66
96
|
* Guard the workspace-delete path before any external/DB mutation. Refuses
|
|
67
97
|
* (409) to delete the account's last workspace, and refuses while any session
|
|
@@ -74,7 +104,9 @@ export function assertWorkspaceDeletable(input: {
|
|
|
74
104
|
activeSessionCount: number;
|
|
75
105
|
}): void {
|
|
76
106
|
if (input.workspaceCountForAccount <= 1) {
|
|
77
|
-
throw new HTTPException(409, {
|
|
107
|
+
throw new HTTPException(409, {
|
|
108
|
+
message: "cannot delete the account's only workspace",
|
|
109
|
+
});
|
|
78
110
|
}
|
|
79
111
|
if (input.activeSessionCount > 0) {
|
|
80
112
|
throw new HTTPException(409, {
|
|
@@ -19,13 +19,20 @@ import type {
|
|
|
19
19
|
EditableArtifactLiveServer,
|
|
20
20
|
OpenEditableArtifactLiveInput,
|
|
21
21
|
} from "./server";
|
|
22
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
EditableArtifactLiveSourceSessionAuthority,
|
|
24
|
+
EditableArtifactLiveTicket,
|
|
25
|
+
} from "./types";
|
|
23
26
|
|
|
24
27
|
export type EditableArtifactLiveCompatibilityRequest = Readonly<{
|
|
25
28
|
artifact: EditableArtifact;
|
|
26
|
-
|
|
29
|
+
modality: EditableArtifactModality;
|
|
30
|
+
liveProtocolVersion: number;
|
|
27
31
|
kernelVersion: string;
|
|
28
32
|
modelSchemaVersion: number;
|
|
33
|
+
snapshotVersion: number;
|
|
34
|
+
commandProtocolVersion: number;
|
|
35
|
+
committedTransactionProtocolVersion: number;
|
|
29
36
|
}>;
|
|
30
37
|
|
|
31
38
|
/** A client/runtime protocol tuple cannot be served by the loaded kernel. */
|
|
@@ -71,10 +78,15 @@ export type ReadEditableArtifactApplicationInput = Readonly<{
|
|
|
71
78
|
|
|
72
79
|
export type MintEditableArtifactApplicationTicketInput = ReadEditableArtifactApplicationInput &
|
|
73
80
|
Readonly<{
|
|
74
|
-
|
|
81
|
+
modality: EditableArtifactModality;
|
|
82
|
+
liveProtocolVersion: number;
|
|
75
83
|
kernelVersion: string;
|
|
76
84
|
modelSchemaVersion: number;
|
|
85
|
+
snapshotVersion: number;
|
|
86
|
+
commandProtocolVersion: number;
|
|
87
|
+
committedTransactionProtocolVersion: number;
|
|
77
88
|
allowEdit: boolean;
|
|
89
|
+
sourceSessionAuthority?: EditableArtifactLiveSourceSessionAuthority;
|
|
78
90
|
}>;
|
|
79
91
|
|
|
80
92
|
/** Exact application seam used by standalone API and embedding hosts. */
|
|
@@ -148,9 +160,19 @@ export class EditableArtifactApplication implements EditableArtifactApplicationP
|
|
|
148
160
|
const artifact = await this.readArtifact(input);
|
|
149
161
|
await this.dependencies.compatibility.assertCompatible({
|
|
150
162
|
artifact,
|
|
151
|
-
|
|
163
|
+
modality: input.modality,
|
|
164
|
+
liveProtocolVersion: positiveInteger(input.liveProtocolVersion, "liveProtocolVersion"),
|
|
152
165
|
kernelVersion: boundedVersion(input.kernelVersion, "kernelVersion"),
|
|
153
166
|
modelSchemaVersion: positiveInteger(input.modelSchemaVersion, "modelSchemaVersion"),
|
|
167
|
+
snapshotVersion: positiveInteger(input.snapshotVersion, "snapshotVersion"),
|
|
168
|
+
commandProtocolVersion: positiveInteger(
|
|
169
|
+
input.commandProtocolVersion,
|
|
170
|
+
"commandProtocolVersion",
|
|
171
|
+
),
|
|
172
|
+
committedTransactionProtocolVersion: positiveInteger(
|
|
173
|
+
input.committedTransactionProtocolVersion,
|
|
174
|
+
"committedTransactionProtocolVersion",
|
|
175
|
+
),
|
|
154
176
|
});
|
|
155
177
|
return await this.dependencies.live.mintTicket({
|
|
156
178
|
scope: artifact.scope,
|
|
@@ -158,6 +180,9 @@ export class EditableArtifactApplication implements EditableArtifactApplicationP
|
|
|
158
180
|
modality: artifact.modality,
|
|
159
181
|
actor: input.actor,
|
|
160
182
|
allowEdit: input.allowEdit,
|
|
183
|
+
...(input.sourceSessionAuthority
|
|
184
|
+
? { sourceSessionAuthority: input.sourceSessionAuthority }
|
|
185
|
+
: {}),
|
|
161
186
|
});
|
|
162
187
|
}
|
|
163
188
|
|
|
@@ -40,7 +40,6 @@ export type EditableArtifactLiveBootstrapRead = Readonly<{
|
|
|
40
40
|
scope: EditableArtifactScope;
|
|
41
41
|
artifactId: EditableArtifactId;
|
|
42
42
|
resume: EditableArtifactLiveResume;
|
|
43
|
-
protocolVersion: number;
|
|
44
43
|
}>;
|
|
45
44
|
|
|
46
45
|
export type EditableArtifactLiveTransactionPage = Readonly<{
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EditableArtifactAuthorizationPort } from "../domain/editable-artifacts/ports";
|
|
2
2
|
import type { EditableArtifactService } from "../domain/editable-artifacts/service";
|
|
3
|
+
import type { Database } from "@opengeni/db";
|
|
3
4
|
import {
|
|
4
5
|
EDITABLE_ARTIFACT_COMMAND_MAX_BYTES,
|
|
5
6
|
EDITABLE_ARTIFACT_INTENT_MAX_BYTES,
|
|
@@ -54,9 +55,13 @@ import {
|
|
|
54
55
|
type EditableArtifactLiveSnapshot,
|
|
55
56
|
type EditableArtifactLiveTicket,
|
|
56
57
|
type EditableArtifactLiveTicketRecord,
|
|
58
|
+
type EditableArtifactLiveSourceSessionAuthority,
|
|
57
59
|
} from "./types";
|
|
58
60
|
|
|
59
61
|
const textEncoder = new TextEncoder();
|
|
62
|
+
// Source-bound sockets must renew through the product proxy's live host check.
|
|
63
|
+
// Unbound native console sockets retain their existing artifact-only lifecycle.
|
|
64
|
+
const SOURCE_SESSION_LIVE_LEASE_MS = 15_000;
|
|
60
65
|
const strictDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
61
66
|
|
|
62
67
|
// Shared OGATX is bounded at 5 MiB and whole OGACO at 8 MiB. Transport
|
|
@@ -81,7 +86,6 @@ const MAX_IDENTIFIER_BYTES = 512;
|
|
|
81
86
|
const MAX_CAUSAL_ACTORS = 1_024;
|
|
82
87
|
|
|
83
88
|
export type EditableArtifactLiveServerOptions = Readonly<{
|
|
84
|
-
protocolVersion?: number;
|
|
85
89
|
ticketTtlMs?: number;
|
|
86
90
|
maxClientFrameBytes?: number;
|
|
87
91
|
maxOutboundFrameBytes?: number;
|
|
@@ -101,7 +105,10 @@ export type EditableArtifactLiveServerOptions = Readonly<{
|
|
|
101
105
|
}>;
|
|
102
106
|
|
|
103
107
|
type NormalizedOptions = Required<Omit<EditableArtifactLiveServerOptions, "ticketTtlMs">> &
|
|
104
|
-
Readonly<{
|
|
108
|
+
Readonly<{
|
|
109
|
+
protocolVersion: typeof EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION;
|
|
110
|
+
ticketTtlMs?: number;
|
|
111
|
+
}>;
|
|
105
112
|
|
|
106
113
|
export type OpenEditableArtifactLiveInput = Readonly<{
|
|
107
114
|
token: string;
|
|
@@ -110,6 +117,13 @@ export type OpenEditableArtifactLiveInput = Readonly<{
|
|
|
110
117
|
resume: EditableArtifactLiveResume;
|
|
111
118
|
sink: EditableArtifactLiveSinkPort;
|
|
112
119
|
signal?: AbortSignal;
|
|
120
|
+
/** API-owned canonical source session authorization, including live revocation. */
|
|
121
|
+
authorizeSourceSession?: (
|
|
122
|
+
ticket: EditableArtifactLiveTicketRecord,
|
|
123
|
+
permission: "read" | "edit",
|
|
124
|
+
transaction?: Database,
|
|
125
|
+
signal?: AbortSignal,
|
|
126
|
+
) => Promise<boolean>;
|
|
113
127
|
}>;
|
|
114
128
|
|
|
115
129
|
export interface EditableArtifactLiveSession {
|
|
@@ -147,7 +161,6 @@ export class EditableArtifactLiveServer {
|
|
|
147
161
|
tickets: dependencies.tickets,
|
|
148
162
|
tokens: dependencies.tokens,
|
|
149
163
|
clock: dependencies.clock,
|
|
150
|
-
protocolVersion: this.options.protocolVersion,
|
|
151
164
|
...(this.options.ticketTtlMs === undefined ? {} : { ttlMs: this.options.ticketTtlMs }),
|
|
152
165
|
});
|
|
153
166
|
}
|
|
@@ -159,6 +172,7 @@ export class EditableArtifactLiveServer {
|
|
|
159
172
|
modality: EditableArtifactModality;
|
|
160
173
|
actor: EditableArtifactActor;
|
|
161
174
|
allowEdit: boolean;
|
|
175
|
+
sourceSessionAuthority?: EditableArtifactLiveSourceSessionAuthority;
|
|
162
176
|
}>,
|
|
163
177
|
): Promise<EditableArtifactLiveTicket> {
|
|
164
178
|
return this.ticketAuthority.mint(input);
|
|
@@ -182,6 +196,9 @@ export class EditableArtifactLiveServer {
|
|
|
182
196
|
resume: input.resume,
|
|
183
197
|
sink: input.sink,
|
|
184
198
|
...(input.signal === undefined ? {} : { parentSignal: input.signal }),
|
|
199
|
+
...(input.authorizeSourceSession
|
|
200
|
+
? { authorizeSourceSession: input.authorizeSourceSession }
|
|
201
|
+
: {}),
|
|
185
202
|
});
|
|
186
203
|
await session.start();
|
|
187
204
|
return session;
|
|
@@ -196,6 +213,7 @@ type SessionConstructor = Readonly<{
|
|
|
196
213
|
resume: EditableArtifactLiveResume;
|
|
197
214
|
sink: EditableArtifactLiveSinkPort;
|
|
198
215
|
parentSignal?: AbortSignal;
|
|
216
|
+
authorizeSourceSession?: OpenEditableArtifactLiveInput["authorizeSourceSession"];
|
|
199
217
|
}>;
|
|
200
218
|
|
|
201
219
|
type InFlight = Readonly<{
|
|
@@ -213,6 +231,8 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
213
231
|
private readonly dependencies: EditableArtifactLiveServerDependencies;
|
|
214
232
|
private readonly options: NormalizedOptions;
|
|
215
233
|
private readonly ticket: EditableArtifactLiveTicketRecord;
|
|
234
|
+
private readonly authorizeSourceSession: OpenEditableArtifactLiveInput["authorizeSourceSession"];
|
|
235
|
+
private readonly sourceSessionLeaseExpiresAt: number | null;
|
|
216
236
|
private readonly resume: EditableArtifactLiveResume;
|
|
217
237
|
private readonly sink: EditableArtifactLiveSinkPort;
|
|
218
238
|
private readonly abort = new AbortController();
|
|
@@ -246,6 +266,10 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
246
266
|
this.dependencies = input.dependencies;
|
|
247
267
|
this.options = input.options;
|
|
248
268
|
this.ticket = input.ticket;
|
|
269
|
+
this.authorizeSourceSession = input.authorizeSourceSession;
|
|
270
|
+
this.sourceSessionLeaseExpiresAt = input.ticket.sourceSessionAuthority
|
|
271
|
+
? Date.parse(input.ticket.issuedAt) + SOURCE_SESSION_LIVE_LEASE_MS
|
|
272
|
+
: null;
|
|
249
273
|
this.artifactId = input.ticket.artifactId;
|
|
250
274
|
this.modality = input.ticket.modality;
|
|
251
275
|
this.streamEpoch = input.streamEpoch;
|
|
@@ -267,6 +291,13 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
267
291
|
async start(): Promise<void> {
|
|
268
292
|
try {
|
|
269
293
|
this.assertOpen();
|
|
294
|
+
// Deadline enforcement owns no work-queue slot and starts before any
|
|
295
|
+
// bootstrap/authorization await. A hung callback cannot retain a socket.
|
|
296
|
+
if (this.sourceSessionLeaseExpiresAt !== null) {
|
|
297
|
+
void this.runSourceSessionLease().catch((error: unknown) => {
|
|
298
|
+
if (!this.abort.signal.aborted) void this.fail(error);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
270
301
|
this.authorizationRevision = await this.requireRead();
|
|
271
302
|
this.assertOpen();
|
|
272
303
|
|
|
@@ -312,7 +343,6 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
312
343
|
scope: this.ticket.scope,
|
|
313
344
|
artifactId: this.artifactId,
|
|
314
345
|
resume: this.resume,
|
|
315
|
-
protocolVersion: this.options.protocolVersion,
|
|
316
346
|
});
|
|
317
347
|
this.assertOpen();
|
|
318
348
|
validateBootstrap(bootstrap, this.artifactId, this.modality, this.resume, this.options);
|
|
@@ -527,6 +557,7 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
527
557
|
}
|
|
528
558
|
|
|
529
559
|
private assertOpen(): void {
|
|
560
|
+
this.assertSourceSessionLease();
|
|
530
561
|
if (this.closedValue) {
|
|
531
562
|
throw new EditableArtifactLiveError("closed", "Live session is closed");
|
|
532
563
|
}
|
|
@@ -706,7 +737,13 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
706
737
|
} as const;
|
|
707
738
|
await this.send(
|
|
708
739
|
snapshot.modality === "spreadsheet"
|
|
709
|
-
? {
|
|
740
|
+
? {
|
|
741
|
+
...common,
|
|
742
|
+
modality: snapshot.modality,
|
|
743
|
+
causalFrontier: causalFrontier!,
|
|
744
|
+
operationProtocolVersion: snapshot.operationProtocolVersion,
|
|
745
|
+
snapshotVersion: snapshot.snapshotVersion,
|
|
746
|
+
}
|
|
710
747
|
: {
|
|
711
748
|
...common,
|
|
712
749
|
modality: snapshot.modality,
|
|
@@ -919,6 +956,25 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
919
956
|
scope: this.ticket.scope,
|
|
920
957
|
artifactId: this.artifactId,
|
|
921
958
|
actor: this.ticket.actor,
|
|
959
|
+
...(this.ticket.sourceSessionAuthority
|
|
960
|
+
? {
|
|
961
|
+
authorizeCommit: async (transaction) => {
|
|
962
|
+
this.assertOpen();
|
|
963
|
+
if (
|
|
964
|
+
!this.authorizeSourceSession ||
|
|
965
|
+
!(await this.withBoundedAuthorization((signal) =>
|
|
966
|
+
this.authorizeSourceSession!(this.ticket, "edit", transaction, signal),
|
|
967
|
+
))
|
|
968
|
+
) {
|
|
969
|
+
throw new EditableArtifactLiveError(
|
|
970
|
+
"permission_changed",
|
|
971
|
+
"Editable artifact source session permission changed",
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
this.assertOpen();
|
|
975
|
+
},
|
|
976
|
+
}
|
|
977
|
+
: {}),
|
|
922
978
|
request: {
|
|
923
979
|
intentBytes: frame.intentBytes.slice(),
|
|
924
980
|
requestHash: frame.requestHash,
|
|
@@ -1029,11 +1085,22 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
1029
1085
|
private async checkPermission(
|
|
1030
1086
|
permission: "read" | "edit",
|
|
1031
1087
|
): Promise<Awaited<ReturnType<EditableArtifactAuthorizationPort["authorize"]>>> {
|
|
1032
|
-
const decision = await this.
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1088
|
+
const decision = await this.withBoundedAuthorization(async (signal) => {
|
|
1089
|
+
if (this.ticket.sourceSessionAuthority) {
|
|
1090
|
+
if (
|
|
1091
|
+
!this.authorizeSourceSession ||
|
|
1092
|
+
!(await this.authorizeSourceSession(this.ticket, permission, undefined, signal))
|
|
1093
|
+
) {
|
|
1094
|
+
return Object.freeze({ allowed: false, revision: this.authorizationRevision });
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
signal.throwIfAborted();
|
|
1098
|
+
return await this.dependencies.authorization.authorize({
|
|
1099
|
+
scope: this.ticket.scope,
|
|
1100
|
+
artifactId: this.artifactId,
|
|
1101
|
+
actor: this.ticket.actor,
|
|
1102
|
+
permission,
|
|
1103
|
+
});
|
|
1037
1104
|
});
|
|
1038
1105
|
if (permission === "edit" && !this.ticket.allowEdit) {
|
|
1039
1106
|
return Object.freeze({ ...decision, allowed: false });
|
|
@@ -1041,6 +1108,57 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
1041
1108
|
return decision;
|
|
1042
1109
|
}
|
|
1043
1110
|
|
|
1111
|
+
private async withBoundedAuthorization<T>(
|
|
1112
|
+
operation: (signal: AbortSignal) => Promise<T>,
|
|
1113
|
+
): Promise<T> {
|
|
1114
|
+
this.assertSourceSessionLease();
|
|
1115
|
+
const timeout = new AbortController();
|
|
1116
|
+
const signal = AbortSignal.any([this.abort.signal, timeout.signal]);
|
|
1117
|
+
// Native artifact-only sockets retain their authorization timing. Only
|
|
1118
|
+
// source-bound sockets have a renewable product-session lease.
|
|
1119
|
+
if (this.sourceSessionLeaseExpiresAt !== null) {
|
|
1120
|
+
const budget = this.sourceSessionLeaseExpiresAt - this.dependencies.clock.now().getTime();
|
|
1121
|
+
void this.dependencies.scheduler.sleep(budget, signal).then(
|
|
1122
|
+
() =>
|
|
1123
|
+
timeout.abort(
|
|
1124
|
+
new EditableArtifactLiveError("ticket_expired", "Live authorization lease expired", {
|
|
1125
|
+
retryable: true,
|
|
1126
|
+
}),
|
|
1127
|
+
),
|
|
1128
|
+
() => undefined,
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
try {
|
|
1132
|
+
const result = await awaitLiveAuthorization(operation(signal), signal);
|
|
1133
|
+
this.assertOpen();
|
|
1134
|
+
return result;
|
|
1135
|
+
} finally {
|
|
1136
|
+
timeout.abort();
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
private async runSourceSessionLease(): Promise<void> {
|
|
1141
|
+
while (!this.abort.signal.aborted && this.sourceSessionLeaseExpiresAt !== null) {
|
|
1142
|
+
const remaining = this.sourceSessionLeaseExpiresAt - this.dependencies.clock.now().getTime();
|
|
1143
|
+
if (remaining <= 0) {
|
|
1144
|
+
await this.close("ticket_expired");
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
await this.dependencies.scheduler.sleep(remaining, this.abort.signal);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
private assertSourceSessionLease(): void {
|
|
1151
|
+
if (
|
|
1152
|
+
this.sourceSessionLeaseExpiresAt !== null &&
|
|
1153
|
+
this.dependencies.clock.now().getTime() >= this.sourceSessionLeaseExpiresAt
|
|
1154
|
+
) {
|
|
1155
|
+
void this.close("ticket_expired");
|
|
1156
|
+
throw new EditableArtifactLiveError("ticket_expired", "Source session lease expired", {
|
|
1157
|
+
retryable: true,
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1044
1162
|
private async sendBarrier(): Promise<void> {
|
|
1045
1163
|
if (!this.sentStateHash)
|
|
1046
1164
|
throw new EditableArtifactLiveError("durable_gap", "Bootstrap has no state boundary");
|
|
@@ -1056,6 +1174,7 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
1056
1174
|
|
|
1057
1175
|
private async send(frame: EditableArtifactLiveServerFrame): Promise<void> {
|
|
1058
1176
|
if (this.closedValue) return;
|
|
1177
|
+
this.assertSourceSessionLease();
|
|
1059
1178
|
const bytes = estimateFrameBytes(frame);
|
|
1060
1179
|
if (bytes > this.options.maxOutboundFrameBytes) {
|
|
1061
1180
|
throw new EditableArtifactLiveError(
|
|
@@ -1125,9 +1244,21 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
1125
1244
|
),
|
|
1126
1245
|
);
|
|
1127
1246
|
while (!this.abort.signal.aborted) {
|
|
1128
|
-
|
|
1247
|
+
const leaseRemaining =
|
|
1248
|
+
this.sourceSessionLeaseExpiresAt === null
|
|
1249
|
+
? tick
|
|
1250
|
+
: this.sourceSessionLeaseExpiresAt - this.dependencies.clock.now().getTime();
|
|
1251
|
+
if (leaseRemaining <= 0) {
|
|
1252
|
+
await this.close("ticket_expired");
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
await this.dependencies.scheduler.sleep(Math.min(tick, leaseRemaining), this.abort.signal);
|
|
1129
1256
|
if (this.abort.signal.aborted || this.closedValue) return;
|
|
1130
1257
|
const now = this.dependencies.clock.now().getTime();
|
|
1258
|
+
if (this.sourceSessionLeaseExpiresAt !== null && now >= this.sourceSessionLeaseExpiresAt) {
|
|
1259
|
+
await this.close("ticket_expired");
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1131
1262
|
if (this.oldestInFlightAt !== 0 && now - this.oldestInFlightAt >= this.options.ackTimeoutMs) {
|
|
1132
1263
|
await this.enqueue(() => this.requireResync("slow_client", this.targetHead));
|
|
1133
1264
|
return;
|
|
@@ -1171,6 +1302,29 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
1171
1302
|
}
|
|
1172
1303
|
}
|
|
1173
1304
|
|
|
1305
|
+
/** Settle callers on cancellation even when an embedding callback ignores it. */
|
|
1306
|
+
function awaitLiveAuthorization<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
1307
|
+
return new Promise<T>((resolve, reject) => {
|
|
1308
|
+
const abort = () => {
|
|
1309
|
+
signal.removeEventListener("abort", abort);
|
|
1310
|
+
reject(signal.reason);
|
|
1311
|
+
};
|
|
1312
|
+
operation.then(
|
|
1313
|
+
(value) => {
|
|
1314
|
+
signal.removeEventListener("abort", abort);
|
|
1315
|
+
if (signal.aborted) reject(signal.reason);
|
|
1316
|
+
else resolve(value);
|
|
1317
|
+
},
|
|
1318
|
+
(error: unknown) => {
|
|
1319
|
+
signal.removeEventListener("abort", abort);
|
|
1320
|
+
reject(error);
|
|
1321
|
+
},
|
|
1322
|
+
);
|
|
1323
|
+
if (signal.aborted) abort();
|
|
1324
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1174
1328
|
function decodeClientFrame(
|
|
1175
1329
|
bytes: Uint8Array,
|
|
1176
1330
|
artifactId: EditableArtifactId,
|
|
@@ -1264,7 +1418,7 @@ function validateResume(
|
|
|
1264
1418
|
resume: EditableArtifactLiveResume,
|
|
1265
1419
|
durableModality: EditableArtifactModality,
|
|
1266
1420
|
): void {
|
|
1267
|
-
const resumeModality = resume.modality
|
|
1421
|
+
const resumeModality = resume.modality;
|
|
1268
1422
|
if (resumeModality !== durableModality) {
|
|
1269
1423
|
throw new EditableArtifactLiveError(
|
|
1270
1424
|
"invalid_frame",
|
|
@@ -1608,7 +1762,7 @@ function boundedString(value: string, label: string, maxBytes: number): void {
|
|
|
1608
1762
|
|
|
1609
1763
|
function normalizeOptions(options: EditableArtifactLiveServerOptions): NormalizedOptions {
|
|
1610
1764
|
const normalized = {
|
|
1611
|
-
protocolVersion:
|
|
1765
|
+
protocolVersion: EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION,
|
|
1612
1766
|
maxClientFrameBytes: options.maxClientFrameBytes ?? DEFAULT_MAX_CLIENT_FRAME_BYTES,
|
|
1613
1767
|
maxOutboundFrameBytes: options.maxOutboundFrameBytes ?? DEFAULT_MAX_OUTBOUND_FRAME_BYTES,
|
|
1614
1768
|
maxSnapshotBytes: options.maxSnapshotBytes ?? DEFAULT_MAX_SNAPSHOT_BYTES,
|