@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.
Files changed (224) hide show
  1. package/README.md +1 -1
  2. package/dist/access/external-actor-authority.d.ts +47 -0
  3. package/dist/access/index.d.ts +182 -3
  4. package/dist/access/service-initiator.d.ts +3 -0
  5. package/dist/application/api-integration-servers.d.ts +20 -0
  6. package/dist/application/archived-session-imports.d.ts +6 -0
  7. package/dist/application/composer-submit.d.ts +24 -0
  8. package/dist/application/connect-authority.d.ts +13 -0
  9. package/dist/application/connect-operation.d.ts +31 -0
  10. package/dist/application/external-continuation.d.ts +15 -0
  11. package/dist/application/external-identity-lifecycle.d.ts +20 -0
  12. package/dist/application/external-link-work-admission.d.ts +12 -0
  13. package/dist/application/external-workspace-members.d.ts +34 -0
  14. package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
  15. package/dist/application/new-session-drafts.d.ts +39 -2
  16. package/dist/application/sandbox-recovery.d.ts +58 -0
  17. package/dist/application/session-commands.d.ts +26 -13
  18. package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
  19. package/dist/application/session-tenancy.d.ts +19 -0
  20. package/dist/application/user-resource-grants.d.ts +20 -0
  21. package/dist/billing/limits.d.ts +11 -0
  22. package/dist/canonical-human-identities.d.ts +7 -2
  23. package/dist/canonical-human-identities.js +17 -4
  24. package/dist/canonical-human-identities.js.map +1 -1
  25. package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
  26. package/dist/chunk-2MKMRGSY.js.map +1 -0
  27. package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
  28. package/dist/chunk-GVO3N37G.js.map +1 -0
  29. package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
  30. package/dist/chunk-LCQXNR62.js.map +1 -0
  31. package/dist/chunk-MM6N7CTO.js +387 -0
  32. package/dist/chunk-MM6N7CTO.js.map +1 -0
  33. package/dist/chunk-QO5GVFFO.js +235 -0
  34. package/dist/chunk-QO5GVFFO.js.map +1 -0
  35. package/dist/codex-model-availability.d.ts +14 -0
  36. package/dist/default-session-model.d.ts +139 -0
  37. package/dist/dependencies.d.ts +87 -4
  38. package/dist/developer-setup-credential-ceiling.d.ts +10 -0
  39. package/dist/domain/agent-config-resolution.d.ts +49 -0
  40. package/dist/domain/automations.d.ts +58 -0
  41. package/dist/domain/capabilities.d.ts +22 -4
  42. package/dist/domain/company-profile-agent-admin.d.ts +25 -0
  43. package/dist/domain/connection-account-selection-error.d.ts +8 -0
  44. package/dist/domain/connector-tool-permissions.d.ts +36 -0
  45. package/dist/domain/direct-model-provider.d.ts +3 -0
  46. package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
  47. package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
  48. package/dist/domain/editable-artifacts/ports.d.ts +10 -0
  49. package/dist/domain/editable-artifacts/service.d.ts +4 -3
  50. package/dist/domain/environments.d.ts +5 -9
  51. package/dist/domain/external-creation-attribution.d.ts +6 -0
  52. package/dist/domain/file-owner.d.ts +8 -0
  53. package/dist/domain/github-action-policies.d.ts +36 -0
  54. package/dist/domain/github-repository-bindings.d.ts +117 -0
  55. package/dist/domain/github-skill-source.d.ts +5 -0
  56. package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
  57. package/dist/domain/insights-timing.d.ts +16 -0
  58. package/dist/domain/insights-usage.d.ts +27 -0
  59. package/dist/domain/insights.d.ts +28 -1
  60. package/dist/domain/knowledge-files.d.ts +20 -0
  61. package/dist/domain/knowledge-messages.d.ts +39 -0
  62. package/dist/domain/knowledge-messages.test.d.ts +1 -0
  63. package/dist/domain/knowledge-preparation.d.ts +10 -0
  64. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  65. package/dist/domain/knowledge-search.d.ts +15 -0
  66. package/dist/domain/knowledge-search.test.d.ts +1 -0
  67. package/dist/domain/knowledge.d.ts +9 -0
  68. package/dist/domain/mcp-account-bindings.d.ts +19 -0
  69. package/dist/domain/mcp-account-routes.d.ts +21 -0
  70. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  71. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  72. package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
  73. package/dist/domain/personal-connection-delegations.d.ts +87 -4
  74. package/dist/domain/pr-review.d.ts +249 -0
  75. package/dist/domain/resources.d.ts +24 -3
  76. package/dist/domain/scheduled-task-access.d.ts +247 -0
  77. package/dist/domain/scheduled-tasks.d.ts +103 -6
  78. package/dist/domain/session-connection-accounts.d.ts +16 -0
  79. package/dist/domain/session-file-access.d.ts +4 -0
  80. package/dist/domain/session-native-connection-replacement.d.ts +14 -0
  81. package/dist/domain/session-start-timing.d.ts +7 -0
  82. package/dist/domain/session-tool-policy.d.ts +93 -4
  83. package/dist/domain/sessions.d.ts +275 -28
  84. package/dist/domain/skill-imports.d.ts +1 -0
  85. package/dist/domain/skill-search.d.ts +30 -0
  86. package/dist/domain/skills.d.ts +117 -0
  87. package/dist/domain/slack-bot.d.ts +46 -1
  88. package/dist/domain/workspace-members.d.ts +8 -1
  89. package/dist/editable-artifact-live/application.d.ts +12 -3
  90. package/dist/editable-artifact-live/ports.d.ts +0 -1
  91. package/dist/editable-artifact-live/server.d.ts +5 -2
  92. package/dist/editable-artifact-live/ticket.d.ts +2 -2
  93. package/dist/editable-artifact-live/types.d.ts +14 -2
  94. package/dist/editable-artifact-live.js +2 -2
  95. package/dist/editable-artifacts.js +10 -4
  96. package/dist/index.d.ts +49 -3
  97. package/dist/index.js +18812 -6833
  98. package/dist/index.js.map +1 -1
  99. package/dist/managed-auth-session-sets.d.ts +131 -0
  100. package/dist/managed-auth-session-sets.js +51 -0
  101. package/dist/managed-auth-session-sets.js.map +1 -0
  102. package/dist/managed-session.d.ts +98 -11
  103. package/dist/model-catalog.d.ts +117 -0
  104. package/dist/product-usage-metrics.d.ts +30 -0
  105. package/dist/rigs/index.d.ts +22 -5
  106. package/dist/rigs/provider-images.d.ts +1 -1
  107. package/dist/sandbox/fleet.d.ts +59 -10
  108. package/dist/sandbox/routing.d.ts +22 -2
  109. package/dist/sandbox/runtime-settings.d.ts +56 -0
  110. package/dist/session-authorization.d.ts +38 -1
  111. package/dist/site-session-origin.d.ts +9 -0
  112. package/dist/transcription.d.ts +12 -1
  113. package/dist/turn-surface.d.ts +18 -0
  114. package/dist/user-presence.d.ts +32 -0
  115. package/package.json +18 -11
  116. package/src/access/external-actor-authority.ts +94 -0
  117. package/src/access/index.ts +1386 -44
  118. package/src/access/service-initiator.ts +50 -0
  119. package/src/application/api-integration-servers.ts +215 -0
  120. package/src/application/archived-session-imports.ts +161 -0
  121. package/src/application/composer-submit.ts +77 -0
  122. package/src/application/connect-authority.ts +77 -0
  123. package/src/application/connect-operation.ts +80 -0
  124. package/src/application/external-continuation.ts +129 -0
  125. package/src/application/external-identity-lifecycle.ts +48 -0
  126. package/src/application/external-link-work-admission.ts +90 -0
  127. package/src/application/external-workspace-members.ts +232 -0
  128. package/src/application/modal-native-worker-host-transport.ts +263 -0
  129. package/src/application/new-session-drafts.ts +222 -29
  130. package/src/application/sandbox-recovery.ts +78 -0
  131. package/src/application/session-commands.ts +430 -73
  132. package/src/application/session-mcp-credential-rotation.ts +217 -0
  133. package/src/application/session-tenancy.ts +445 -0
  134. package/src/application/user-resource-grants.ts +154 -0
  135. package/src/billing/limits.ts +103 -24
  136. package/src/canonical-human-identities.ts +22 -3
  137. package/src/codex-model-availability.ts +136 -0
  138. package/src/default-session-model.ts +656 -0
  139. package/src/dependencies.ts +97 -5
  140. package/src/developer-setup-credential-ceiling.ts +60 -0
  141. package/src/domain/agent-config-resolution.ts +148 -0
  142. package/src/domain/automations.ts +270 -0
  143. package/src/domain/capabilities.ts +244 -151
  144. package/src/domain/company-profile-agent-admin.ts +56 -0
  145. package/src/domain/connection-account-selection-error.ts +19 -0
  146. package/src/domain/connector-tool-permissions.ts +371 -0
  147. package/src/domain/direct-model-provider.ts +62 -0
  148. package/src/domain/editable-artifacts/agent-application.ts +61 -15
  149. package/src/domain/editable-artifacts/durable-export.ts +100 -5
  150. package/src/domain/editable-artifacts/in-memory.ts +6 -0
  151. package/src/domain/editable-artifacts/ports.ts +11 -0
  152. package/src/domain/editable-artifacts/service.ts +32 -1
  153. package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
  154. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
  155. package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
  156. package/src/domain/environments.ts +25 -45
  157. package/src/domain/external-creation-attribution.ts +22 -0
  158. package/src/domain/file-owner.ts +67 -0
  159. package/src/domain/github-action-policies.ts +169 -0
  160. package/src/domain/github-repository-bindings.ts +295 -0
  161. package/src/domain/github-skill-source.ts +233 -0
  162. package/src/domain/governed-learning-slack-publication.ts +157 -0
  163. package/src/domain/insights-timing.ts +95 -0
  164. package/src/domain/insights-usage.ts +67 -0
  165. package/src/domain/insights.ts +270 -169
  166. package/src/domain/knowledge-files.ts +46 -0
  167. package/src/domain/knowledge-messages.test.ts +91 -0
  168. package/src/domain/knowledge-messages.ts +106 -0
  169. package/src/domain/knowledge-preparation.test.ts +276 -0
  170. package/src/domain/knowledge-preparation.ts +147 -0
  171. package/src/domain/knowledge-search.test.ts +561 -0
  172. package/src/domain/knowledge-search.ts +256 -0
  173. package/src/domain/knowledge.ts +133 -0
  174. package/src/domain/mcp-account-bindings.ts +171 -0
  175. package/src/domain/mcp-account-routes.ts +121 -0
  176. package/src/domain/native-mcp-connection-admission.ts +14 -0
  177. package/src/domain/organization-integration-catalog.ts +88 -0
  178. package/src/domain/organization-membership-lifecycle.ts +10 -0
  179. package/src/domain/personal-connection-delegations.ts +758 -115
  180. package/src/domain/pr-review.ts +687 -0
  181. package/src/domain/resources.ts +182 -25
  182. package/src/domain/scheduled-task-access.ts +1052 -0
  183. package/src/domain/scheduled-tasks.ts +1952 -215
  184. package/src/domain/session-connection-accounts.ts +67 -0
  185. package/src/domain/session-file-access.ts +47 -0
  186. package/src/domain/session-native-connection-replacement.ts +64 -0
  187. package/src/domain/session-start-timing.ts +55 -0
  188. package/src/domain/session-tool-policy.ts +443 -19
  189. package/src/domain/sessions.ts +3417 -619
  190. package/src/domain/skill-imports.ts +137 -17
  191. package/src/domain/skill-search.ts +221 -0
  192. package/src/domain/skills.ts +104 -0
  193. package/src/domain/slack-bot.ts +128 -3
  194. package/src/domain/workspace-members.ts +34 -2
  195. package/src/editable-artifact-live/application.ts +29 -4
  196. package/src/editable-artifact-live/ports.ts +0 -1
  197. package/src/editable-artifact-live/server.ts +167 -13
  198. package/src/editable-artifact-live/ticket.ts +87 -6
  199. package/src/editable-artifact-live/types.ts +16 -2
  200. package/src/editable-artifact-live/wire.ts +0 -10
  201. package/src/index.ts +69 -4
  202. package/src/managed-auth-session-sets.ts +371 -0
  203. package/src/managed-session.ts +520 -2
  204. package/src/model-catalog.ts +873 -0
  205. package/src/product-usage-metrics.ts +101 -0
  206. package/src/rigs/index.ts +179 -63
  207. package/src/rigs/provider-images.ts +2 -3
  208. package/src/sandbox/fleet.ts +471 -59
  209. package/src/sandbox/routing.ts +160 -33
  210. package/src/sandbox/runtime-settings.ts +281 -0
  211. package/src/session-authorization.ts +262 -78
  212. package/src/site-session-origin.ts +22 -0
  213. package/src/transcription.ts +13 -1
  214. package/src/turn-surface.ts +45 -0
  215. package/src/user-presence.ts +134 -0
  216. package/dist/chunk-6WKPWE5S.js.map +0 -1
  217. package/dist/chunk-IBOEYG6N.js +0 -34
  218. package/dist/chunk-IBOEYG6N.js.map +0 -1
  219. package/dist/chunk-JJU6I5XD.js.map +0 -1
  220. package/dist/chunk-NYPMQ7NO.js.map +0 -1
  221. package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
  222. package/dist/domain/packs.d.ts +0 -55
  223. package/src/domain/durable-learning-slack-publication.ts +0 -202
  224. package/src/domain/packs.ts +0 -556
@@ -0,0 +1,217 @@
1
+ import { createHmac } from "node:crypto";
2
+ import { RotateSessionMcpCredentialsRequest, type AccessGrant } from "@opengeni/contracts";
3
+ import {
4
+ encryptVariableSetValue,
5
+ getWorkspaceGrant,
6
+ subjectHasLiveWorkspaceAuthorityInScope,
7
+ type Database,
8
+ } from "@opengeni/db";
9
+ import {
10
+ rotateSessionMcpCredentialsAtomically,
11
+ SessionMcpCredentialRotationError,
12
+ } from "@opengeni/db/session-mcp-credential-rotation";
13
+ import { HTTPException } from "hono/http-exception";
14
+ import type { Settings } from "@opengeni/config";
15
+ import {
16
+ requirePermission,
17
+ hasVerifiedOwningUserAuthorization,
18
+ requireResolvedAccessGrantAuthorization,
19
+ type AccessGrantAuthorization,
20
+ } from "../access";
21
+ import {
22
+ grantHasAgentAttemptAuthority,
23
+ requireSessionAuthorization,
24
+ SessionAuthorizationDeniedError,
25
+ SessionAuthorizationUnavailableError,
26
+ type SessionAuthorizationDependencies,
27
+ } from "../session-authorization";
28
+ import { requireVariableSetEncryption } from "../domain/environments";
29
+ import { normalizedSessionMcpCredentialHeaders } from "../domain/sessions";
30
+ import { externalContinuationCommitAuthorizer } from "./external-continuation";
31
+ import {
32
+ listOwnConnectionAccountsForGrant,
33
+ personalConnectionDelegationSourceForGrant,
34
+ } from "../domain/personal-connection-delegations";
35
+ import { nativeSessionConnectionReplacement } from "../domain/session-native-connection-replacement";
36
+
37
+ /** HMAC domains separate request identity from key availability. The key tag
38
+ * carries no request material and neither fingerprint is returned publicly. */
39
+ export function sessionMcpRotationFingerprint(
40
+ key: Uint8Array,
41
+ scope: { accountId: string; workspaceId: string; sessionId: string; subjectId: string },
42
+ request: RotateSessionMcpCredentialsRequest,
43
+ ) {
44
+ return {
45
+ digestKeyTag: createHmac("sha256", key)
46
+ .update("opengeni:session-mcp-rotation:key:v1")
47
+ .digest("hex"),
48
+ requestDigest: createHmac("sha256", key)
49
+ .update("opengeni:session-mcp-rotation:request:v1\0")
50
+ .update(JSON.stringify({ ...scope, request }))
51
+ .digest("hex"),
52
+ };
53
+ }
54
+
55
+ function requireRotationGrant(grant: AccessGrant) {
56
+ if (grantHasAgentAttemptAuthority(grant))
57
+ throw new HTTPException(403, { message: "host authorization required" });
58
+ requirePermission(grant, "sessions:control");
59
+ requirePermission(grant, "mcp_servers:attach");
60
+ }
61
+
62
+ /** HTTP/SDK host boundary only. The mandatory callback re-resolves request
63
+ * authentication with the transaction handle, not a cached route-time grant. */
64
+ export async function rotateSessionMcpCredentialsForRequest(
65
+ deps: SessionAuthorizationDependencies & { settings: Settings },
66
+ authorization: AccessGrantAuthorization,
67
+ sessionId: string,
68
+ raw: unknown,
69
+ reauthorize: (tx: Database) => Promise<AccessGrant>,
70
+ ) {
71
+ const grant = requireResolvedAccessGrantAuthorization(
72
+ authorization,
73
+ authorization.grant.workspaceId,
74
+ );
75
+ requireRotationGrant(grant);
76
+ const parsed = RotateSessionMcpCredentialsRequest.safeParse(raw);
77
+ if (!parsed.success)
78
+ throw new HTTPException(422, { message: "invalid credential rotation request" });
79
+ const encryptionKey = requireVariableSetEncryption(deps.settings);
80
+ let request: RotateSessionMcpCredentialsRequest;
81
+ try {
82
+ request = {
83
+ operationKey: parsed.data.operationKey,
84
+ updates: parsed.data.updates
85
+ .map((update) => ({
86
+ id: update.id,
87
+ expectedCredentialVersion: update.expectedCredentialVersion,
88
+ expectedServerUrl: update.expectedServerUrl,
89
+ ...("nativeConnectionId" in update
90
+ ? {
91
+ nativeConnectionId: update.nativeConnectionId,
92
+ ...(update.replacementServerUrl
93
+ ? { replacementServerUrl: update.replacementServerUrl }
94
+ : {}),
95
+ }
96
+ : {
97
+ headers: Object.fromEntries(
98
+ Object.entries(normalizedSessionMcpCredentialHeaders(update.headers))
99
+ .map(([name, value]) => [name.toLowerCase(), value] as const)
100
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
101
+ ),
102
+ }),
103
+ }))
104
+ .sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0)),
105
+ };
106
+ if (new Set(request.updates.map((update) => update.id)).size !== request.updates.length)
107
+ throw new Error();
108
+ } catch {
109
+ // Header names and validation paths are caller-controlled secret channels.
110
+ throw new HTTPException(422, { message: "invalid credential rotation headers or server IDs" });
111
+ }
112
+ const scope = {
113
+ accountId: grant.accountId,
114
+ workspaceId: grant.workspaceId,
115
+ sessionId,
116
+ subjectId: grant.subjectId,
117
+ actorType: grant.principalKind === "service" ? ("service" as const) : ("human" as const),
118
+ };
119
+ const externalAuthorize = externalContinuationCommitAuthorizer(authorization);
120
+ let authorizedGrant = grant;
121
+ try {
122
+ return await rotateSessionMcpCredentialsAtomically(deps.db, {
123
+ ...scope,
124
+ operationKey: request.operationKey,
125
+ ...sessionMcpRotationFingerprint(encryptionKey, scope, request),
126
+ updates: request.updates.map((update) =>
127
+ "nativeConnectionId" in update
128
+ ? update
129
+ : {
130
+ id: update.id,
131
+ expectedCredentialVersion: update.expectedCredentialVersion,
132
+ expectedServerUrl: update.expectedServerUrl,
133
+ headersEncrypted: Object.fromEntries(
134
+ Object.entries(update.headers).map(([name, value]) => [
135
+ name,
136
+ encryptVariableSetValue(encryptionKey, value),
137
+ ]),
138
+ ),
139
+ },
140
+ ),
141
+ resolveNativeConnection: async (tx, server, nativeConnectionId, replacementServerUrl) => {
142
+ const connections = await listOwnConnectionAccountsForGrant(tx, authorizedGrant);
143
+ const source = personalConnectionDelegationSourceForGrant(authorizedGrant);
144
+ try {
145
+ return nativeSessionConnectionReplacement({
146
+ accountId: grant.accountId,
147
+ workspaceId: grant.workspaceId,
148
+ subjectId: source.kind === "subject" ? source.subjectId : null,
149
+ serverUrl: server.url,
150
+ ...(replacementServerUrl ? { replacementServerUrl } : {}),
151
+ currentRef: server.connectionRef,
152
+ nativeConnectionId,
153
+ connections,
154
+ });
155
+ } catch {
156
+ throw new SessionMcpCredentialRotationError("connection_unavailable");
157
+ }
158
+ },
159
+ authorize: async (tx) => {
160
+ await externalAuthorize?.(tx);
161
+ const fresh = await reauthorize(tx);
162
+ if (
163
+ fresh.accountId !== grant.accountId ||
164
+ fresh.workspaceId !== grant.workspaceId ||
165
+ fresh.subjectId !== grant.subjectId ||
166
+ fresh.principalKind !== grant.principalKind
167
+ )
168
+ throw new HTTPException(403, { message: "credential rotation authority changed" });
169
+ requireRotationGrant(fresh);
170
+ if (fresh.principalKind === "human_session" && !externalAuthorize) {
171
+ // Signed host grants may narrow a named human's permissions, never
172
+ // outlive removal or expand that human's current workspace grant.
173
+ const live = await getWorkspaceGrant(tx, fresh.subjectId, fresh.workspaceId);
174
+ if (live) {
175
+ requireRotationGrant(live);
176
+ } else if (!hasVerifiedOwningUserAuthorization(authorization)) {
177
+ throw new HTTPException(403, { message: "credential rotation authority changed" });
178
+ }
179
+ if (!(await subjectHasLiveWorkspaceAuthorityInScope(tx, scope))) {
180
+ throw new HTTPException(403, { message: "credential rotation authority changed" });
181
+ }
182
+ }
183
+ await requireSessionAuthorization({ ...deps, db: tx }, fresh, {
184
+ sessionId,
185
+ operation: "session.mcp.credentials.rotate",
186
+ surface: "http",
187
+ });
188
+ authorizedGrant = fresh;
189
+ },
190
+ });
191
+ } catch (error) {
192
+ if (error instanceof HTTPException)
193
+ throw new HTTPException(error.status, { message: "credential rotation access denied" });
194
+ if (error instanceof SessionAuthorizationDeniedError)
195
+ throw new HTTPException(403, { message: "credential rotation access denied" });
196
+ if (error instanceof SessionAuthorizationUnavailableError)
197
+ throw new HTTPException(503, { message: "credential rotation authorization unavailable" });
198
+ if (error instanceof SessionMcpCredentialRotationError) {
199
+ const status =
200
+ error.code === "receipt_key_unavailable"
201
+ ? 503
202
+ : error.code === "authority_revoked"
203
+ ? 403
204
+ : error.code === "not_found"
205
+ ? 404
206
+ : error.code === "invalid_request" ||
207
+ error.code === "brokered_server" ||
208
+ error.code === "connection_unavailable"
209
+ ? 422
210
+ : 409;
211
+ throw new HTTPException(status, { message: `credential_rotation_${error.code}` });
212
+ }
213
+ // Drizzle failures can include query parameters. Never attach their cause,
214
+ // ciphertext, fingerprints, header names, or raw input to API telemetry.
215
+ throw new HTTPException(503, { message: "credential rotation unavailable" });
216
+ }
217
+ }
@@ -0,0 +1,445 @@
1
+ import {
2
+ ForkSessionResponse,
3
+ SessionTenancyCreateCapabilities,
4
+ UpdateSessionVisibilityResponse,
5
+ sessionVisibilityFromPublic,
6
+ sessionVisibilityToPublic,
7
+ type ForkSessionRequest,
8
+ type SessionAuthorizationSurface,
9
+ type UpdateSessionVisibilityRequest,
10
+ type VariableSet,
11
+ } from "@opengeni/contracts";
12
+ import {
13
+ assertSessionIsNotImported,
14
+ forkSessionContent,
15
+ getSessionForSubject,
16
+ getRig,
17
+ getPrivateSessionCreatePolicy,
18
+ getSessionEventForSubject,
19
+ isRetryableDatabaseTransportFailure,
20
+ nestedPostgresSqlState,
21
+ replayAppliedSessionFork,
22
+ sessionTenancyProductActivated,
23
+ SessionTenancyAccessError,
24
+ SessionTenancyInvalidRequestError,
25
+ SessionTenancyNotActivatedError,
26
+ transitionSessionVisibility,
27
+ type Database,
28
+ type ForkSessionContentResult,
29
+ } from "@opengeni/db";
30
+ import { publishDurableSessionEvents, type EventBus } from "@opengeni/events";
31
+ import {
32
+ requirePermission,
33
+ hasVerifiedOwningUserAuthorization,
34
+ type AccessGrantAuthorization,
35
+ } from "../access";
36
+ import { requireSessionAuthorization } from "../session-authorization";
37
+ import type { AppDependencies } from "../dependencies";
38
+ import { validateVariableSetAttachment } from "../domain/environments";
39
+ import { externalContinuationCommitAuthorizer } from "./external-continuation";
40
+
41
+ type SessionTenancyDependencies = Pick<
42
+ AppDependencies,
43
+ "db" | "bus" | "sessionAuthorization" | "settings"
44
+ >;
45
+
46
+ export class SessionTenancyManagedHumanRequiredError extends Error {
47
+ readonly name = "SessionTenancyManagedHumanRequiredError";
48
+ constructor() {
49
+ super("Session tenancy mutations require the owning managed-human session");
50
+ }
51
+ }
52
+
53
+ export class SessionTenancyPersistenceOutcomeUnknownError extends Error {
54
+ readonly name = "SessionTenancyPersistenceOutcomeUnknownError";
55
+ constructor(options?: ErrorOptions) {
56
+ super("The session tenancy mutation outcome is unknown", options);
57
+ }
58
+ }
59
+
60
+ export function requireCanonicalManagedHuman(
61
+ authorization: AccessGrantAuthorization,
62
+ workspaceId: string,
63
+ ): void {
64
+ if (
65
+ !authorization.canonicalManagedHumanSession ||
66
+ !authorization.contextIntegrity ||
67
+ authorization.authenticatedSubjectId !== authorization.grant.subjectId ||
68
+ authorization.grant.workspaceId !== workspaceId
69
+ ) {
70
+ throw new SessionTenancyManagedHumanRequiredError();
71
+ }
72
+ }
73
+
74
+ export function requireVerifiedOwningUser(
75
+ authorization: AccessGrantAuthorization,
76
+ workspaceId: string,
77
+ ): void {
78
+ if (
79
+ !hasVerifiedOwningUserAuthorization(authorization) ||
80
+ authorization.grant.workspaceId !== workspaceId
81
+ ) {
82
+ throw new SessionTenancyManagedHumanRequiredError();
83
+ }
84
+ }
85
+
86
+ export async function getManagedHumanSessionCreateCapabilities(
87
+ deps: Pick<SessionTenancyDependencies, "db">,
88
+ authorization: AccessGrantAuthorization,
89
+ workspaceId: string,
90
+ ): Promise<SessionTenancyCreateCapabilities> {
91
+ requirePermission(authorization.grant, "sessions:create");
92
+ try {
93
+ requireVerifiedOwningUser(authorization, workspaceId);
94
+ } catch (error) {
95
+ if (error instanceof SessionTenancyManagedHumanRequiredError) {
96
+ return SessionTenancyCreateCapabilities.parse({
97
+ activated: false,
98
+ canCreatePrivate: false,
99
+ reason: "managed_session_required",
100
+ });
101
+ }
102
+ throw error;
103
+ }
104
+ // Mirror the database create fence exactly: a managed human's own Personal
105
+ // workspace needs only the operator readiness receipt, while a shared
106
+ // organization workspace additionally needs the owner/admin product setting.
107
+ let activated = false;
108
+ try {
109
+ const policy = await getPrivateSessionCreatePolicy(deps.db, {
110
+ workspaceId,
111
+ actorSubjectId: authorization.grant.subjectId,
112
+ });
113
+ activated = policy.personalWorkspace ? policy.platformAvailable : policy.organizationEnabled;
114
+ } catch (error) {
115
+ // A canonical managed human without an active organization membership or
116
+ // stated workspace authority (legacy managed access) is simply not
117
+ // private-ready; the definer's 42501 is a capability answer here, not a
118
+ // failure. Every other error still propagates.
119
+ if (
120
+ !(error instanceof SessionTenancyAccessError) &&
121
+ nestedPostgresSqlState(error) !== "42501"
122
+ ) {
123
+ throw error;
124
+ }
125
+ }
126
+ return SessionTenancyCreateCapabilities.parse({
127
+ activated,
128
+ canCreatePrivate: activated,
129
+ reason: activated ? "available" : "not_activated",
130
+ });
131
+ }
132
+
133
+ export async function requireManagedHumanPrivateSessionCreate(
134
+ deps: Pick<SessionTenancyDependencies, "db">,
135
+ authorization: AccessGrantAuthorization,
136
+ workspaceId: string,
137
+ ): Promise<void> {
138
+ // Target-free readiness preflight only. The owner/admin organization setting
139
+ // is enforced by the create transaction itself (under the organization
140
+ // fence, after keyed replay resolution), so a committed keyed success still
141
+ // replays after the setting is disabled while a fresh key fails closed.
142
+ await requireSessionTenancyMutationGate(deps, authorization, workspaceId, ["sessions:create"]);
143
+ }
144
+
145
+ async function requireSessionTenancyMutationGate(
146
+ deps: Pick<SessionTenancyDependencies, "db">,
147
+ authorization: AccessGrantAuthorization,
148
+ workspaceId: string,
149
+ permissions: ReadonlyArray<"sessions:read" | "sessions:create" | "sessions:control">,
150
+ ): Promise<void> {
151
+ // These checks are deliberately target-free. A rejected principal must not
152
+ // cause a session lookup or embedding-host callback that distinguishes a
153
+ // missing, shared, or another owner's private session.
154
+ requireVerifiedOwningUser(authorization, workspaceId);
155
+ for (const permission of permissions) requirePermission(authorization.grant, permission);
156
+ if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
157
+ throw new SessionTenancyNotActivatedError();
158
+ }
159
+ }
160
+
161
+ async function publishExactCommittedEvent(
162
+ deps: { db: Database; bus: EventBus },
163
+ input: {
164
+ workspaceId: string;
165
+ sessionId: string;
166
+ subjectId: string;
167
+ eventId: string | null;
168
+ eventSequence: number | null;
169
+ },
170
+ ): Promise<void> {
171
+ if (input.eventId === null && input.eventSequence === null) return;
172
+ if (input.eventId === null || input.eventSequence === null) {
173
+ console.error("[session-tenancy] committed event receipt is incomplete", {
174
+ errorClass: "SessionTenancyEventInvariantError",
175
+ errorCode: "session_tenancy_event_receipt_incomplete",
176
+ origin: "core",
177
+ sessionId: input.sessionId,
178
+ });
179
+ return;
180
+ }
181
+ try {
182
+ const event = await getSessionEventForSubject(
183
+ deps.db,
184
+ input.workspaceId,
185
+ input.subjectId,
186
+ input.eventId,
187
+ );
188
+ if (!event || event.sessionId !== input.sessionId || event.sequence !== input.eventSequence) {
189
+ console.error("[session-tenancy] committed event receipt did not resolve exactly", {
190
+ errorClass: "SessionTenancyEventInvariantError",
191
+ errorCode: "session_tenancy_event_receipt_mismatch",
192
+ origin: "core",
193
+ sessionId: input.sessionId,
194
+ });
195
+ return;
196
+ }
197
+ await publishDurableSessionEvents(deps.bus, input.workspaceId, input.sessionId, [event]);
198
+ } catch {
199
+ // The mutation and event committed atomically before this read. Live fanout
200
+ // is repairable from the durable stream, so a publication-side failure must
201
+ // not turn a known successful mutation into an unknown client outcome.
202
+ console.warn("[session-tenancy] committed event live publication was deferred to replay", {
203
+ errorClass: "SessionTenancyEventPublishOperationError",
204
+ errorCode: "session_tenancy_event_publish_deferred",
205
+ origin: "core",
206
+ sessionId: input.sessionId,
207
+ });
208
+ }
209
+ }
210
+
211
+ async function runTenancyMutation<T>(operation: () => Promise<T>): Promise<T> {
212
+ try {
213
+ return await operation();
214
+ } catch (error) {
215
+ if (isRetryableDatabaseTransportFailure(error)) {
216
+ throw new SessionTenancyPersistenceOutcomeUnknownError({ cause: error });
217
+ }
218
+ throw error;
219
+ }
220
+ }
221
+
222
+ async function projectAndPublishSessionFork(
223
+ deps: SessionTenancyDependencies,
224
+ actorSubjectId: string,
225
+ result: ForkSessionContentResult,
226
+ ): Promise<ForkSessionResponse> {
227
+ const response = ForkSessionResponse.parse({
228
+ operationId: result.operationId,
229
+ eventId: result.eventId,
230
+ eventSequence: result.eventSequence,
231
+ sessionId: result.sessionId,
232
+ workspaceId: result.workspaceId,
233
+ visibility: sessionVisibilityToPublic(result.visibility),
234
+ authorityEpoch: result.authorityEpoch,
235
+ copiedHistoryItemCount: result.copiedHistoryItemCount,
236
+ replay: result.replay,
237
+ });
238
+ await publishExactCommittedEvent(deps, {
239
+ workspaceId: response.workspaceId,
240
+ sessionId: response.sessionId,
241
+ subjectId: actorSubjectId,
242
+ eventId: response.eventId,
243
+ eventSequence: response.eventSequence,
244
+ });
245
+ return response;
246
+ }
247
+
248
+ export async function updateManagedHumanSessionVisibility(
249
+ deps: SessionTenancyDependencies,
250
+ authorization: AccessGrantAuthorization,
251
+ workspaceId: string,
252
+ sessionId: string,
253
+ request: UpdateSessionVisibilityRequest,
254
+ authorizationSurface: SessionAuthorizationSurface = "core",
255
+ ): Promise<UpdateSessionVisibilityResponse> {
256
+ const beforeCommit = externalContinuationCommitAuthorizer(authorization);
257
+ await requireSessionTenancyMutationGate(deps, authorization, workspaceId, ["sessions:control"]);
258
+ await requireSessionAuthorization(deps, authorization.grant, {
259
+ sessionId,
260
+ operation: "session.visibility.write",
261
+ surface: authorizationSurface,
262
+ });
263
+
264
+ const result = await runTenancyMutation(
265
+ async () =>
266
+ await transitionSessionVisibility(deps.db, {
267
+ workspaceId,
268
+ sessionId,
269
+ actorSubjectId: authorization.grant.subjectId,
270
+ targetVisibility: sessionVisibilityFromPublic(request.visibility),
271
+ expectedAuthorityEpoch: request.expectedAuthorityEpoch,
272
+ operationKey: request.idempotencyKey,
273
+ ...(beforeCommit ? { beforeCommit } : {}),
274
+ }),
275
+ );
276
+ const response = UpdateSessionVisibilityResponse.parse({
277
+ operationId: result.operationId,
278
+ eventId: result.eventId,
279
+ eventSequence: result.eventSequence,
280
+ visibility: sessionVisibilityToPublic(result.visibility),
281
+ authorityEpoch: result.authorityEpoch,
282
+ changed: result.changed,
283
+ replay: result.replay,
284
+ revokedGrantCount: result.revokedGrantCount,
285
+ });
286
+ await publishExactCommittedEvent(deps, {
287
+ workspaceId,
288
+ sessionId,
289
+ subjectId: authorization.grant.subjectId,
290
+ eventId: response.eventId,
291
+ eventSequence: response.eventSequence,
292
+ });
293
+ return response;
294
+ }
295
+
296
+ export async function forkManagedHumanSession(
297
+ deps: SessionTenancyDependencies,
298
+ authorization: AccessGrantAuthorization,
299
+ workspaceId: string,
300
+ sourceSessionId: string,
301
+ request: ForkSessionRequest,
302
+ authorizationSurface: SessionAuthorizationSurface = "core",
303
+ ): Promise<ForkSessionResponse> {
304
+ await requireSessionTenancyMutationGate(deps, authorization, workspaceId, [
305
+ "sessions:read",
306
+ "sessions:create",
307
+ ]);
308
+ const beforeCommit = externalContinuationCommitAuthorizer(authorization);
309
+ const forkInput = {
310
+ sourceWorkspaceId: workspaceId,
311
+ sourceSessionId,
312
+ ...(request.sourceEventId ? { sourceEventId: request.sourceEventId } : {}),
313
+ actorSubjectId: authorization.grant.subjectId,
314
+ destinationWorkspaceId: workspaceId,
315
+ destinationVisibility:
316
+ request.visibility === "private" ? ("user_private" as const) : ("workspace_shared" as const),
317
+ workspaceSharedAcknowledged: request.workspaceSharedAcknowledged,
318
+ operationKey: request.idempotencyKey,
319
+ ...(beforeCommit ? { beforeCommit } : {}),
320
+ ...(request.rigId !== undefined || request.variableSetIds !== undefined
321
+ ? {
322
+ runtimeRequest: {
323
+ rigId: request.rigId ?? null,
324
+ variableSetIds: request.variableSetIds ?? [],
325
+ },
326
+ }
327
+ : {}),
328
+ };
329
+ const recoverAppliedReceipt = async () =>
330
+ await runTenancyMutation(async () => await replayAppliedSessionFork(deps.db, forkInput));
331
+ const runtimeSetupRequested = request.rigId !== undefined || request.variableSetIds !== undefined;
332
+ let runtimeConfigurationPromise: Promise<{
333
+ variableSetIds: string[];
334
+ rigId: string | null;
335
+ rigVersionId: string | null;
336
+ }> | null = null;
337
+ const resolveRuntimeConfiguration = async () => {
338
+ if (runtimeConfigurationPromise) return await runtimeConfigurationPromise;
339
+ runtimeConfigurationPromise = (async () => {
340
+ const variableSets: VariableSet[] = [];
341
+ for (const variableSetId of request.variableSetIds ?? []) {
342
+ variableSets.push(
343
+ await validateVariableSetAttachment(
344
+ { settings: deps.settings, db: deps.db },
345
+ authorization.grant,
346
+ workspaceId,
347
+ variableSetId,
348
+ ),
349
+ );
350
+ }
351
+ let rigVersionId: string | null = null;
352
+ if (request.rigId) {
353
+ requirePermission(authorization.grant, "rigs:use");
354
+ const rig = await getRig(deps.db, authorization.grant, request.rigId);
355
+ if (!rig?.activeVersion) {
356
+ throw new SessionTenancyInvalidRequestError();
357
+ }
358
+ for (const defaultVariableSetId of new Set(rig.activeVersion.defaultVariableSetIds)) {
359
+ await validateVariableSetAttachment(
360
+ { settings: deps.settings, db: deps.db },
361
+ authorization.grant,
362
+ workspaceId,
363
+ defaultVariableSetId,
364
+ );
365
+ }
366
+ rigVersionId = rig.activeVersion.id;
367
+ }
368
+ return {
369
+ variableSetIds: variableSets.map((variableSet) => variableSet.id),
370
+ rigId: request.rigId ?? null,
371
+ rigVersionId,
372
+ };
373
+ })();
374
+ return await runtimeConfigurationPromise;
375
+ };
376
+ const publishRuntimeConfiguration = async (result: ForkSessionContentResult) => {
377
+ if (!result.runtimeEventId || !result.runtimeEventSequence) return;
378
+ await publishExactCommittedEvent(deps, {
379
+ workspaceId,
380
+ sessionId: result.sessionId,
381
+ subjectId: authorization.grant.subjectId,
382
+ eventId: result.runtimeEventId,
383
+ eventSequence: result.runtimeEventSequence,
384
+ });
385
+ };
386
+
387
+ // A committed response is owned by the exact actor/key/request tuple and no
388
+ // longer depends on mutable source visibility. Resolve it before source/host
389
+ // authorization so a lost response remains recoverable after the source
390
+ // owner makes a formerly shared session private.
391
+ const applied = await recoverAppliedReceipt();
392
+ if (applied) {
393
+ const response = await projectAndPublishSessionFork(
394
+ deps,
395
+ authorization.grant.subjectId,
396
+ applied,
397
+ );
398
+ await publishRuntimeConfiguration(applied);
399
+ return response;
400
+ }
401
+
402
+ try {
403
+ await requireSessionAuthorization(deps, authorization.grant, {
404
+ sessionId: sourceSessionId,
405
+ operation: "session.fork.create",
406
+ surface: authorizationSurface,
407
+ });
408
+ } catch (authorizationError) {
409
+ // Close the race where a duplicate request checked before the first commit
410
+ // but mutable source/host authority changed immediately after it. Only an
411
+ // exact applied receipt can convert the authorization failure into replay.
412
+ const racedApplied = await recoverAppliedReceipt();
413
+ if (racedApplied) {
414
+ const response = await projectAndPublishSessionFork(
415
+ deps,
416
+ authorization.grant.subjectId,
417
+ racedApplied,
418
+ );
419
+ await publishRuntimeConfiguration(racedApplied);
420
+ return response;
421
+ }
422
+ throw authorizationError;
423
+ }
424
+
425
+ // Validate all requested runtime resources before committing the independent
426
+ // fork so ordinary authorization failures cannot leave an unusable copy.
427
+ const source = await getSessionForSubject(
428
+ deps.db,
429
+ workspaceId,
430
+ sourceSessionId,
431
+ authorization.grant.subjectId,
432
+ );
433
+ if (source) assertSessionIsNotImported(source);
434
+ const runtimeConfiguration = runtimeSetupRequested ? await resolveRuntimeConfiguration() : null;
435
+ const result = await runTenancyMutation(
436
+ async () =>
437
+ await forkSessionContent(deps.db, {
438
+ ...forkInput,
439
+ ...(runtimeConfiguration ? { runtimeConfiguration } : {}),
440
+ }),
441
+ );
442
+ const response = await projectAndPublishSessionFork(deps, authorization.grant.subjectId, result);
443
+ await publishRuntimeConfiguration(result);
444
+ return response;
445
+ }